mirror of
https://github.com/QuivrHQ/quivr.git
synced 2024-12-14 17:03:29 +03:00
feat(frontend): sharepoint and gdrive integration (#2643)
# 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
47c6e24bf1
commit
3d3e6b7306
@ -8,6 +8,9 @@ from modules.sync.dto.inputs import SyncsUserInput, SyncUserUpdateInput
|
||||
from modules.sync.service.sync_service import SyncService, SyncUserService
|
||||
from modules.user.entity.user_identity import UserIdentity
|
||||
from msal import PublicClientApplication
|
||||
from .successfull_connection import successfullConnectionPage
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
|
||||
# Initialize logger
|
||||
logger = get_logger(__name__)
|
||||
@ -31,7 +34,7 @@ SCOPE = [
|
||||
]
|
||||
|
||||
|
||||
@azure_sync_router.get(
|
||||
@azure_sync_router.post(
|
||||
"/sync/azure/authorize",
|
||||
dependencies=[Depends(AuthBearer())],
|
||||
tags=["Sync"],
|
||||
@ -125,4 +128,4 @@ def oauth2callback_azure(request: Request):
|
||||
|
||||
sync_user_service.update_sync_user(current_user, state_dict, sync_user_input)
|
||||
logger.info(f"Azure sync created successfully for user: {current_user}")
|
||||
return {"message": "Azure sync created successfully"}
|
||||
return HTMLResponse(successfullConnectionPage)
|
||||
|
@ -9,6 +9,9 @@ from middlewares.auth import AuthBearer, get_current_user
|
||||
from modules.sync.dto.inputs import SyncsUserInput, SyncUserUpdateInput
|
||||
from modules.sync.service.sync_service import SyncService, SyncUserService
|
||||
from modules.user.entity.user_identity import UserIdentity
|
||||
from .successfull_connection import successfullConnectionPage
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
|
||||
# Set environment variable for OAuthlib
|
||||
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
|
||||
@ -47,7 +50,7 @@ CLIENT_SECRETS_FILE_CONTENT = {
|
||||
}
|
||||
|
||||
|
||||
@google_sync_router.get(
|
||||
@google_sync_router.post(
|
||||
"/sync/google/authorize",
|
||||
dependencies=[Depends(AuthBearer())],
|
||||
tags=["Sync"],
|
||||
@ -138,4 +141,4 @@ def oauth2callback_google(request: Request):
|
||||
)
|
||||
sync_user_service.update_sync_user(current_user, state_dict, sync_user_input)
|
||||
logger.info(f"Google Drive sync created successfully for user: {current_user}")
|
||||
return {"message": "Google Drive sync created successfully"}
|
||||
return HTMLResponse(successfullConnectionPage)
|
||||
|
53
backend/modules/sync/controller/successfull_connection.py
Normal file
53
backend/modules/sync/controller/successfull_connection.py
Normal file
@ -0,0 +1,53 @@
|
||||
successfullConnectionPage = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
background-color: #f8f9fa;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
}
|
||||
.message {
|
||||
font-size: 2em;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.icon {
|
||||
font-size: 2em;
|
||||
color: white;
|
||||
background-color: green;
|
||||
border-radius: 50%;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.close-button {
|
||||
padding: 10px 20px;
|
||||
font-size: 1em;
|
||||
color: #fff;
|
||||
background-color: #6142d4;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.close-button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<i class="fas fa-check icon"></i>
|
||||
<div class="message">Connection successful</div>
|
||||
<button class="close-button" onclick="window.close();">Close Tab</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
@ -16,7 +16,7 @@ module.exports = {
|
||||
complexity: ["error", 10],
|
||||
"max-lines": ["error", 300],
|
||||
"max-depth": ["error", 3],
|
||||
"max-params": ["error", 4],
|
||||
"max-params": ["error", 5],
|
||||
eqeqeq: ["error", "smart"],
|
||||
"import/no-extraneous-dependencies": [
|
||||
"error",
|
||||
|
@ -24,7 +24,9 @@ import { UserSettingsProvider } from "@/lib/context/UserSettingsProvider/User-se
|
||||
import { IntercomProvider } from "@/lib/helpers/intercom/IntercomProvider";
|
||||
import { UpdateMetadata } from "@/lib/helpers/updateMetadata";
|
||||
import { usePageTracking } from "@/services/analytics/june/usePageTracking";
|
||||
|
||||
import "../lib/config/LocaleConfig/i18n";
|
||||
import { FromConnectionsProvider } from "./chat/[chatId]/components/ActionsBar/components/KnowledgeToFeed/components/FromConnections/FromConnectionsProvider/FromConnection-provider";
|
||||
|
||||
if (
|
||||
process.env.NEXT_PUBLIC_POSTHOG_KEY != null &&
|
||||
@ -90,11 +92,13 @@ const AppWithQueryClient = ({ children }: PropsWithChildren): JSX.Element => {
|
||||
<BrainCreationProvider>
|
||||
<MenuProvider>
|
||||
<OnboardingProvider>
|
||||
<ChatsProvider>
|
||||
<ChatProvider>
|
||||
<App>{children}</App>
|
||||
</ChatProvider>
|
||||
</ChatsProvider>
|
||||
<FromConnectionsProvider>
|
||||
<ChatsProvider>
|
||||
<ChatProvider>
|
||||
<App>{children}</App>
|
||||
</ChatProvider>
|
||||
</ChatsProvider>
|
||||
</FromConnectionsProvider>
|
||||
</OnboardingProvider>
|
||||
</MenuProvider>
|
||||
</BrainCreationProvider>
|
||||
|
@ -9,6 +9,7 @@
|
||||
width: 100%;
|
||||
gap: Spacings.$spacing05;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
|
||||
.single_selector_wrapper {
|
||||
width: 30%;
|
||||
@ -21,44 +22,8 @@
|
||||
|
||||
.tabs_content_wrapper {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.uploaded_knowledges_title {
|
||||
color: var(--text-2);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.uploaded_knowledges {
|
||||
padding: Spacings.$spacing03;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 80%;
|
||||
overflow: scroll;
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing02;
|
||||
flex-grow: 1;
|
||||
overflow: scroll;
|
||||
|
||||
.uploaded_knowledge {
|
||||
display: flex;
|
||||
gap: Spacings.$spacing02;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
font-size: Typography.$small;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: Spacings.$spacing02;
|
||||
overflow: hidden;
|
||||
|
||||
.label {
|
||||
@include Typography.EllipsisOverflow;
|
||||
}
|
||||
}
|
||||
}
|
||||
padding: Spacings.$spacing01;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { Icon } from "@/lib/components/ui/Icon/Icon";
|
||||
import { useSync } from "@/lib/api/sync/useSync";
|
||||
import { SingleSelector } from "@/lib/components/ui/SingleSelector/SingleSelector";
|
||||
import { Tabs } from "@/lib/components/ui/Tabs/Tabs";
|
||||
import { requiredRolesForUpload } from "@/lib/config/upload";
|
||||
@ -9,6 +9,8 @@ import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider
|
||||
import { Tab } from "@/lib/types/Tab";
|
||||
|
||||
import styles from "./KnowledgeToFeed.module.scss";
|
||||
import { FromConnections } from "./components/FromConnections/FromConnections";
|
||||
import { useFromConnectionsContext } from "./components/FromConnections/FromConnectionsProvider/hooks/useFromConnectionContext";
|
||||
import { FromDocuments } from "./components/FromDocuments/FromDocuments";
|
||||
import { FromWebsites } from "./components/FromWebsites/FromWebsites";
|
||||
import { formatMinimalBrainsToSelectComponentInput } from "./utils/formatMinimalBrainsToSelectComponentInput";
|
||||
@ -18,10 +20,13 @@ export const KnowledgeToFeed = ({
|
||||
}: {
|
||||
hideBrainSelector?: boolean;
|
||||
}): JSX.Element => {
|
||||
const { allBrains, setCurrentBrainId, currentBrain } = useBrainContext();
|
||||
const [selectedTab, setSelectedTab] = useState("From documents");
|
||||
const { knowledgeToFeed, removeKnowledgeToFeed } =
|
||||
useKnowledgeToFeedContext();
|
||||
const { allBrains, setCurrentBrainId, currentBrainId, currentBrain } =
|
||||
useBrainContext();
|
||||
const [selectedTab, setSelectedTab] = useState("Documents");
|
||||
const { knowledgeToFeed } = useKnowledgeToFeedContext();
|
||||
const { openedConnections, setOpenedConnections, setCurrentSyncId } =
|
||||
useFromConnectionsContext();
|
||||
const { getActiveSyncsForBrain } = useSync();
|
||||
|
||||
const brainsWithUploadRights = formatMinimalBrainsToSelectComponentInput(
|
||||
useMemo(
|
||||
@ -36,19 +41,69 @@ export const KnowledgeToFeed = ({
|
||||
|
||||
const knowledgesTabs: Tab[] = [
|
||||
{
|
||||
label: "From documents",
|
||||
isSelected: selectedTab === "From documents",
|
||||
onClick: () => setSelectedTab("From documents"),
|
||||
label: "Documents",
|
||||
isSelected: selectedTab === "Documents",
|
||||
onClick: () => setSelectedTab("Documents"),
|
||||
iconName: "file",
|
||||
badge: knowledgeToFeed.filter(
|
||||
(knowledge) => knowledge.source === "upload"
|
||||
).length,
|
||||
},
|
||||
{
|
||||
label: "From websites",
|
||||
isSelected: selectedTab === "From websites",
|
||||
onClick: () => setSelectedTab("From websites"),
|
||||
label: "Websites",
|
||||
isSelected: selectedTab === "Websites",
|
||||
onClick: () => setSelectedTab("Websites"),
|
||||
iconName: "website",
|
||||
badge: knowledgeToFeed.filter((knowledge) => knowledge.source === "crawl")
|
||||
.length,
|
||||
},
|
||||
{
|
||||
label: "Connections",
|
||||
isSelected: selectedTab === "Connections",
|
||||
onClick: () => setSelectedTab("Connections"),
|
||||
iconName: "sync",
|
||||
badge: openedConnections.filter((connection) => connection.submitted)
|
||||
.length,
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (currentBrain) {
|
||||
void (async () => {
|
||||
try {
|
||||
const res = await getActiveSyncsForBrain(currentBrain.id);
|
||||
setCurrentSyncId(undefined);
|
||||
setOpenedConnections(
|
||||
res.map((sync) => ({
|
||||
user_sync_id: sync.syncs_user_id,
|
||||
id: sync.id,
|
||||
provider: sync.syncs_user.provider,
|
||||
submitted: true,
|
||||
selectedFiles: {
|
||||
files: [
|
||||
...(sync.settings.folders?.map((folder) => ({
|
||||
id: folder,
|
||||
name: undefined,
|
||||
is_folder: true,
|
||||
})) ?? []),
|
||||
...(sync.settings.files?.map((file) => ({
|
||||
id: file,
|
||||
name: undefined,
|
||||
is_folder: false,
|
||||
})) ?? []),
|
||||
],
|
||||
},
|
||||
name: sync.name,
|
||||
last_synced: sync.last_synced,
|
||||
}))
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, [currentBrainId]);
|
||||
|
||||
return (
|
||||
<div className={styles.knowledge_to_feed_wrapper}>
|
||||
{!hideBrainSelector && (
|
||||
@ -68,39 +123,9 @@ export const KnowledgeToFeed = ({
|
||||
)}
|
||||
<Tabs tabList={knowledgesTabs} />
|
||||
<div className={styles.tabs_content_wrapper}>
|
||||
{selectedTab === "From documents" && <FromDocuments />}
|
||||
{selectedTab === "From websites" && <FromWebsites />}
|
||||
</div>
|
||||
<div>
|
||||
<div className={styles.uploaded_knowledges_title}>
|
||||
<span>Knowledges to upload</span>
|
||||
<span>{knowledgeToFeed.length}</span>
|
||||
</div>
|
||||
<div className={styles.uploaded_knowledges}>
|
||||
{knowledgeToFeed.map((knowledge, index) => (
|
||||
<div className={styles.uploaded_knowledge} key={index}>
|
||||
<div className={styles.left}>
|
||||
<Icon
|
||||
name={knowledge.source === "crawl" ? "website" : "file"}
|
||||
size="small"
|
||||
color="black"
|
||||
/>
|
||||
<span className={styles.label}>
|
||||
{knowledge.source === "crawl"
|
||||
? knowledge.url
|
||||
: knowledge.file.name}
|
||||
</span>
|
||||
</div>
|
||||
<Icon
|
||||
name="delete"
|
||||
size="normal"
|
||||
color="dangerous"
|
||||
handleHover={true}
|
||||
onClick={() => removeKnowledgeToFeed(index)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{selectedTab === "Documents" && <FromDocuments />}
|
||||
{selectedTab === "Websites" && <FromWebsites />}
|
||||
{selectedTab === "Connections" && <FromConnections />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -0,0 +1,22 @@
|
||||
import { SyncElementLine } from "../SyncElementLine/SyncElementLine";
|
||||
|
||||
interface FileLineProps {
|
||||
name: string;
|
||||
selectable: boolean;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export const FileLine = ({
|
||||
name,
|
||||
selectable,
|
||||
id,
|
||||
}: FileLineProps): JSX.Element => {
|
||||
return (
|
||||
<SyncElementLine
|
||||
name={name}
|
||||
selectable={selectable}
|
||||
id={id}
|
||||
isFolder={false}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,22 @@
|
||||
import { SyncElementLine } from "../SyncElementLine/SyncElementLine";
|
||||
|
||||
interface FolderLineProps {
|
||||
name: string;
|
||||
selectable: boolean;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export const FolderLine = ({
|
||||
name,
|
||||
selectable,
|
||||
id,
|
||||
}: FolderLineProps): JSX.Element => {
|
||||
return (
|
||||
<SyncElementLine
|
||||
name={name}
|
||||
selectable={selectable}
|
||||
id={id}
|
||||
isFolder={true}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,36 @@
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
@use "@/styles/Typography.module.scss";
|
||||
|
||||
.from_connection_container {
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
padding: Spacings.$spacing01;
|
||||
|
||||
.from_connection_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing06;
|
||||
overflow: hidden;
|
||||
max-height: 100%;
|
||||
|
||||
.header_buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.connection_content {
|
||||
overflow: auto;
|
||||
flex-grow: 1;
|
||||
|
||||
&.disable {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.empty_folder {
|
||||
font-style: italic;
|
||||
font-size: Typography.$small;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { SyncElement } from "@/lib/api/sync/types";
|
||||
import { useSync } from "@/lib/api/sync/useSync";
|
||||
import { ConnectionCards } from "@/lib/components/ConnectionCards/ConnectionCards";
|
||||
import TextButton from "@/lib/components/ui/TextButton/TextButton";
|
||||
import { useUserData } from "@/lib/hooks/useUserData";
|
||||
|
||||
import { FileLine } from "./FileLine/FileLine";
|
||||
import { FolderLine } from "./FolderLine/FolderLine";
|
||||
import styles from "./FromConnections.module.scss";
|
||||
import { useFromConnectionsContext } from "./FromConnectionsProvider/hooks/useFromConnectionContext";
|
||||
|
||||
export const FromConnections = (): JSX.Element => {
|
||||
const [folderStack, setFolderStack] = useState<(string | null)[]>([]);
|
||||
const { currentSyncElements, setCurrentSyncElements, currentSyncId } =
|
||||
useFromConnectionsContext();
|
||||
const [currentFiles, setCurrentFiles] = useState<SyncElement[]>([]);
|
||||
const [currentFolders, setCurrentFolders] = useState<SyncElement[]>([]);
|
||||
const { getSyncFiles } = useSync();
|
||||
const { userData } = useUserData();
|
||||
|
||||
const isPremium = userData?.is_premium;
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentFiles(
|
||||
currentSyncElements?.files.filter((file) => !file.is_folder) ?? []
|
||||
);
|
||||
setCurrentFolders(
|
||||
currentSyncElements?.files.filter((file) => file.is_folder) ?? []
|
||||
);
|
||||
}, [currentSyncElements]);
|
||||
|
||||
const handleGetSyncFiles = async (
|
||||
userSyncId: number,
|
||||
folderId: string | null
|
||||
) => {
|
||||
try {
|
||||
let res;
|
||||
if (folderId !== null) {
|
||||
res = await getSyncFiles(userSyncId, folderId);
|
||||
} else {
|
||||
res = await getSyncFiles(userSyncId);
|
||||
}
|
||||
setCurrentSyncElements(res);
|
||||
} catch (error) {
|
||||
console.error("Failed to get sync files:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBackClick = async () => {
|
||||
if (folderStack.length > 0 && currentSyncId) {
|
||||
const newFolderStack = [...folderStack];
|
||||
newFolderStack.pop();
|
||||
setFolderStack(newFolderStack);
|
||||
const parentFolderId = newFolderStack[newFolderStack.length - 1];
|
||||
await handleGetSyncFiles(currentSyncId, parentFolderId);
|
||||
} else {
|
||||
setCurrentSyncElements({ files: [] });
|
||||
}
|
||||
};
|
||||
|
||||
const handleFolderClick = async (userSyncId: number, folderId: string) => {
|
||||
setFolderStack([...folderStack, folderId]);
|
||||
await handleGetSyncFiles(userSyncId, folderId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.from_connection_container}>
|
||||
{!currentSyncId ? (
|
||||
<ConnectionCards fromAddKnowledge={true} />
|
||||
) : (
|
||||
<div className={styles.from_connection_wrapper}>
|
||||
<div className={styles.header_buttons}>
|
||||
<TextButton
|
||||
label="Back"
|
||||
iconName="chevronLeft"
|
||||
color="black"
|
||||
onClick={() => {
|
||||
void handleBackClick();
|
||||
}}
|
||||
small={true}
|
||||
disabled={!folderStack.length}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.connection_content}>
|
||||
{currentFolders.map((folder) => (
|
||||
<div
|
||||
key={folder.id}
|
||||
onClick={() => {
|
||||
void handleFolderClick(currentSyncId, folder.id);
|
||||
}}
|
||||
>
|
||||
<FolderLine
|
||||
name={folder.name ?? ""}
|
||||
selectable={!!isPremium}
|
||||
id={folder.id}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{currentFiles.map((file) => (
|
||||
<div key={file.id}>
|
||||
<FileLine
|
||||
name={file.name ?? ""}
|
||||
selectable={true}
|
||||
id={file.id}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{!currentFiles.length && !currentFolders.length && (
|
||||
<span className={styles.empty_folder}>Empty folder</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,57 @@
|
||||
import { createContext, useState } from "react";
|
||||
|
||||
import { OpenedConnection, SyncElements } from "@/lib/api/sync/types";
|
||||
|
||||
export type FromConnectionsContextType = {
|
||||
currentSyncElements: SyncElements | undefined;
|
||||
setCurrentSyncElements: React.Dispatch<
|
||||
React.SetStateAction<SyncElements | undefined>
|
||||
>;
|
||||
currentSyncId: number | undefined;
|
||||
setCurrentSyncId: React.Dispatch<React.SetStateAction<number | undefined>>;
|
||||
openedConnections: OpenedConnection[];
|
||||
setOpenedConnections: React.Dispatch<
|
||||
React.SetStateAction<OpenedConnection[]>
|
||||
>;
|
||||
hasToReload: boolean;
|
||||
setHasToReload: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
export const FromConnectionsContext = createContext<
|
||||
FromConnectionsContextType | undefined
|
||||
>(undefined);
|
||||
|
||||
export const FromConnectionsProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element => {
|
||||
const [currentSyncElements, setCurrentSyncElements] = useState<
|
||||
SyncElements | undefined
|
||||
>(undefined);
|
||||
const [currentSyncId, setCurrentSyncId] = useState<number | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [openedConnections, setOpenedConnections] = useState<
|
||||
OpenedConnection[]
|
||||
>([]);
|
||||
const [hasToReload, setHasToReload] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<FromConnectionsContext.Provider
|
||||
value={{
|
||||
currentSyncElements,
|
||||
setCurrentSyncElements,
|
||||
currentSyncId,
|
||||
setCurrentSyncId,
|
||||
openedConnections,
|
||||
setOpenedConnections,
|
||||
|
||||
hasToReload,
|
||||
setHasToReload,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</FromConnectionsContext.Provider>
|
||||
);
|
||||
};
|
@ -0,0 +1,17 @@
|
||||
import { useContext } from "react";
|
||||
|
||||
import {
|
||||
FromConnectionsContext,
|
||||
FromConnectionsContextType,
|
||||
} from "../FromConnection-provider";
|
||||
|
||||
export const useFromConnectionsContext = (): FromConnectionsContextType => {
|
||||
const context = useContext(FromConnectionsContext);
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
"useFromConnectionsContext must be used within a FromConnectionsProvider"
|
||||
);
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
@ -0,0 +1,28 @@
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
@use "@/styles/Typography.module.scss";
|
||||
|
||||
.sync_element_line_wrapper {
|
||||
display: flex;
|
||||
gap: Spacings.$spacing03;
|
||||
padding: Spacings.$spacing03;
|
||||
border-top: 1px solid var(--border-1);
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
|
||||
.element_name {
|
||||
font-size: Typography.$small;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--background-3);
|
||||
}
|
||||
|
||||
&.no_hover {
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--background-0);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { Checkbox } from "@/lib/components/ui/Checkbox/Checkbox";
|
||||
import Icon from "@/lib/components/ui/Icon/Icon";
|
||||
|
||||
import styles from "./SyncElementLine.module.scss";
|
||||
|
||||
import { useFromConnectionsContext } from "../FromConnectionsProvider/hooks/useFromConnectionContext";
|
||||
|
||||
interface SyncElementLineProps {
|
||||
name: string;
|
||||
selectable: boolean;
|
||||
id: string;
|
||||
isFolder: boolean;
|
||||
}
|
||||
|
||||
export const SyncElementLine = ({
|
||||
name,
|
||||
selectable,
|
||||
id,
|
||||
isFolder,
|
||||
}: SyncElementLineProps): JSX.Element => {
|
||||
const [isCheckboxHovered, setIsCheckboxHovered] = useState(false);
|
||||
const { currentSyncId, openedConnections, setOpenedConnections } =
|
||||
useFromConnectionsContext();
|
||||
|
||||
const initialChecked = (): boolean => {
|
||||
const currentConnection = openedConnections.find(
|
||||
(connection) => connection.user_sync_id === currentSyncId
|
||||
);
|
||||
|
||||
return currentConnection
|
||||
? currentConnection.selectedFiles.files.some((file) => file.id === id)
|
||||
: false;
|
||||
};
|
||||
|
||||
const [checked, setChecked] = useState<boolean>(initialChecked);
|
||||
|
||||
const handleSetChecked = () => {
|
||||
setOpenedConnections((prevState) => {
|
||||
return prevState.map((connection) => {
|
||||
if (connection.user_sync_id === currentSyncId) {
|
||||
const isFileSelected = connection.selectedFiles.files.some(
|
||||
(file) => file.id === id
|
||||
);
|
||||
const updatedFiles = isFileSelected
|
||||
? connection.selectedFiles.files.filter((file) => file.id !== id)
|
||||
: [
|
||||
...connection.selectedFiles.files,
|
||||
{ id, name, is_folder: isFolder },
|
||||
];
|
||||
|
||||
return {
|
||||
...connection,
|
||||
selectedFiles: {
|
||||
files: updatedFiles,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return connection;
|
||||
});
|
||||
});
|
||||
setChecked((prevChecked) => !prevChecked);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.sync_element_line_wrapper} ${
|
||||
isCheckboxHovered || !isFolder || checked ? styles.no_hover : ""
|
||||
}`}
|
||||
onClick={(event) => {
|
||||
if (isFolder && checked) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={styles.checkbox_wrapper}
|
||||
onMouseEnter={() => setIsCheckboxHovered(true)}
|
||||
onMouseLeave={() => setIsCheckboxHovered(false)}
|
||||
style={{ pointerEvents: "auto" }}
|
||||
>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
setChecked={handleSetChecked}
|
||||
disabled={!selectable}
|
||||
tooltip={
|
||||
!selectable
|
||||
? "Only premium members can sync folders. This feature automatically adds new files from your folders to your brain, keeping it up-to-date without manual effort."
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Icon name={isFolder ? "folder" : "file"} color="black" size="normal" />
|
||||
<span className={styles.element_name}>{name}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -4,33 +4,39 @@
|
||||
|
||||
.from_document_wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
column-gap: Spacings.$spacing05;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 1px dashed var(--border-0);
|
||||
border-radius: Radius.$big;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
overflow: scroll;
|
||||
|
||||
&.dragging {
|
||||
border: 3px dashed var(--accent);
|
||||
background-color: var(--background-3);
|
||||
}
|
||||
|
||||
.input {
|
||||
.box_content {
|
||||
padding: Spacings.$spacing05;
|
||||
display: flex;
|
||||
gap: Spacings.$spacing02;
|
||||
flex-direction: column;
|
||||
column-gap: Spacings.$spacing05;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
@media (max-width: ScreenSizes.$small) {
|
||||
flex-direction: column;
|
||||
}
|
||||
.input {
|
||||
display: flex;
|
||||
gap: Spacings.$spacing02;
|
||||
padding: Spacings.$spacing05;
|
||||
|
||||
.clickable {
|
||||
font-weight: bold;
|
||||
@media (max-width: ScreenSizes.$small) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,13 +27,15 @@ export const FromDocuments = (): JSX.Element => {
|
||||
onMouseLeave={() => setDragging(false)}
|
||||
onClick={open}
|
||||
>
|
||||
<Icon name="upload" size="big" color={dragging ? "accent" : "black"} />
|
||||
<div className={styles.input}>
|
||||
<div className={styles.clickable}>
|
||||
<span>Choose files</span>
|
||||
<input {...getInputProps()} />
|
||||
<div className={styles.box_content}>
|
||||
<Icon name="upload" size="big" color={dragging ? "accent" : "black"} />
|
||||
<div className={styles.input}>
|
||||
<div className={styles.clickable}>
|
||||
<span>Choose files</span>
|
||||
<input {...getInputProps()} />
|
||||
</div>
|
||||
<span>or drag it here</span>
|
||||
</div>
|
||||
<span>or drag it here</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -17,6 +17,7 @@ import { useToast } from "@/lib/hooks";
|
||||
import { useOnboarding } from "@/lib/hooks/useOnboarding";
|
||||
|
||||
import { FeedItemCrawlType, FeedItemUploadType } from "../../../types";
|
||||
import { useFromConnectionsContext } from "../components/FromConnections/FromConnectionsProvider/hooks/useFromConnectionContext";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useFeedBrainInChat = ({
|
||||
@ -42,6 +43,7 @@ export const useFeedBrainInChat = ({
|
||||
const fetchedNotifications = await getChatNotifications(currentChatId);
|
||||
setNotifications(fetchedNotifications);
|
||||
};
|
||||
const { openedConnections } = useFromConnectionsContext();
|
||||
const { crawlWebsiteHandler, uploadFileHandler } = useKnowledgeToFeedInput();
|
||||
const files: File[] = (
|
||||
knowledgeToFeed.filter((c) => c.source === "upload") as FeedItemUploadType[]
|
||||
@ -58,7 +60,7 @@ export const useFeedBrainInChat = ({
|
||||
|
||||
return;
|
||||
}
|
||||
if (knowledgeToFeed.length === 0) {
|
||||
if (knowledgeToFeed.length === 0 && !openedConnections.length) {
|
||||
publish({
|
||||
variant: "danger",
|
||||
text: t("addFiles"),
|
||||
|
@ -85,8 +85,8 @@ div:focus {
|
||||
|
||||
body.dark_mode {
|
||||
/* Backgrounds */
|
||||
--background-0: var(--black-0);
|
||||
--background-1: var(--black-1);
|
||||
--background-0: var(--black-1);
|
||||
--background-1: var(--black-0);
|
||||
--background-2: var(--black-2);
|
||||
--background-3: var(--black-3);
|
||||
--background-4: var(--black-4);
|
||||
|
@ -1,20 +1,15 @@
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
@use "@/styles/Typography.module.scss";
|
||||
|
||||
.cards_wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.connections_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing05;
|
||||
padding: Spacings.$spacing06;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
.title {
|
||||
@include Typography.H2;
|
||||
}
|
||||
|
||||
.brains_grid {
|
||||
display: flex;
|
||||
gap: Spacings.$spacing03;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
12
frontend/app/user/components/Connections/Connections.tsx
Normal file
12
frontend/app/user/components/Connections/Connections.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { ConnectionCards } from "@/lib/components/ConnectionCards/ConnectionCards";
|
||||
|
||||
import styles from "./Connections.module.scss";
|
||||
|
||||
export const Connections = (): JSX.Element => {
|
||||
return (
|
||||
<div className={styles.connections_wrapper}>
|
||||
<span className={styles.title}>Link apps you want to search across</span>
|
||||
<ConnectionCards />
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,9 +1,15 @@
|
||||
@use "@/styles/Radius.module.scss";
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
@use "@/styles/Typography.module.scss";
|
||||
|
||||
.settings_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing07;
|
||||
width: auto;
|
||||
padding: Spacings.$spacing06;
|
||||
|
||||
.title {
|
||||
@include Typography.H2;
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,9 @@ type InfoDisplayerProps = {
|
||||
export const Settings = ({ email }: InfoDisplayerProps): JSX.Element => {
|
||||
return (
|
||||
<div className={styles.settings_wrapper}>
|
||||
<span className={styles.title}>
|
||||
General settings and main information
|
||||
</span>
|
||||
<InfoDisplayer label="Email" iconName="email">
|
||||
<span>{email}</span>
|
||||
</InfoDisplayer>
|
||||
|
@ -1,3 +1,5 @@
|
||||
@use "@/styles/Radius.module.scss";
|
||||
@use "@/styles/ScreenSizes.module.scss";
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
|
||||
.user_page_container {
|
||||
@ -5,7 +7,18 @@
|
||||
padding-block: Spacings.$spacing07;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing05;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@media (max-width: ScreenSizes.$small) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content_wrapper {
|
||||
display: flex;
|
||||
gap: Spacings.$spacing05;
|
||||
}
|
||||
}
|
||||
|
||||
.modal_wrapper {
|
||||
|
@ -7,11 +7,14 @@ import { useUserApi } from "@/lib/api/user/useUserApi";
|
||||
import PageHeader from "@/lib/components/PageHeader/PageHeader";
|
||||
import { Modal } from "@/lib/components/ui/Modal/Modal";
|
||||
import QuivrButton from "@/lib/components/ui/QuivrButton/QuivrButton";
|
||||
import { Tabs } from "@/lib/components/ui/Tabs/Tabs";
|
||||
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
||||
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 { Connections } from "./components/Connections/Connections";
|
||||
import { Settings } from "./components/Settings/Settings";
|
||||
import styles from "./page.module.scss";
|
||||
|
||||
@ -30,6 +33,7 @@ const UserPage = (): JSX.Element => {
|
||||
isLogoutModalOpened,
|
||||
setIsLogoutModalOpened,
|
||||
} = useLogoutModal();
|
||||
const [selectedTab, setSelectedTab] = useState("Connections");
|
||||
|
||||
const buttons: ButtonType[] = [
|
||||
{
|
||||
@ -50,6 +54,21 @@ const UserPage = (): JSX.Element => {
|
||||
},
|
||||
];
|
||||
|
||||
const studioTabs: Tab[] = [
|
||||
{
|
||||
label: "Connections",
|
||||
isSelected: selectedTab === "Connections",
|
||||
onClick: () => setSelectedTab("Connections"),
|
||||
iconName: "sync",
|
||||
},
|
||||
{
|
||||
label: "General",
|
||||
isSelected: selectedTab === "General",
|
||||
onClick: () => setSelectedTab("General"),
|
||||
iconName: "user",
|
||||
},
|
||||
];
|
||||
|
||||
if (!session || !userData) {
|
||||
redirectToLogin();
|
||||
}
|
||||
@ -60,8 +79,11 @@ const UserPage = (): JSX.Element => {
|
||||
<PageHeader iconName="user" label="Profile" buttons={buttons} />
|
||||
</div>
|
||||
<div className={styles.user_page_container}>
|
||||
<Tabs tabList={studioTabs} />
|
||||
<div className={styles.user_page_menu}></div>
|
||||
<div className={styles.content_wrapper}>
|
||||
<Settings email={userData.email} />
|
||||
{selectedTab === "General" && <Settings email={userData.email} />}
|
||||
{selectedTab === "Connections" && <Connections />}
|
||||
</div>
|
||||
</div>
|
||||
<Modal
|
||||
|
@ -1,31 +0,0 @@
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { useAuthApi } from "../useAuthApi";
|
||||
|
||||
const axiosPostMock = vi.fn(() => ({
|
||||
data: {
|
||||
api_key: "",
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/hooks", () => ({
|
||||
useAxios: () => ({
|
||||
axiosInstance: {
|
||||
post: axiosPostMock,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("useAuthApi", () => {
|
||||
it("should call createApiKey with the correct parameters", async () => {
|
||||
const {
|
||||
result: {
|
||||
current: { createApiKey },
|
||||
},
|
||||
} = renderHook(() => useAuthApi());
|
||||
await createApiKey();
|
||||
expect(axiosPostMock).toHaveBeenCalledTimes(1);
|
||||
expect(axiosPostMock).toHaveBeenCalledWith("/api-key");
|
||||
});
|
||||
});
|
@ -1,28 +0,0 @@
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { getNock } from "../../tests/getNock";
|
||||
import { useChatApi } from "../useChatApi";
|
||||
|
||||
getNock().options("/chat").reply(200);
|
||||
|
||||
describe("useChatApi", () => {
|
||||
it("should make http request while creating chat", async () => {
|
||||
const chatName = "Test Chat";
|
||||
|
||||
const scope = getNock().post("/chat").reply(200, { chat_name: chatName });
|
||||
|
||||
const {
|
||||
result: {
|
||||
current: { createChat },
|
||||
},
|
||||
} = renderHook(() => useChatApi());
|
||||
|
||||
const createdChat = await createChat(chatName);
|
||||
|
||||
//Check that the endpoint was called
|
||||
expect(scope.isDone()).toBe(true);
|
||||
|
||||
expect(createdChat).toMatchObject({ chat_name: chatName });
|
||||
});
|
||||
});
|
@ -1,154 +0,0 @@
|
||||
/* eslint-disable max-lines */
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { ChatQuestion } from "@/app/chat/[chatId]/types";
|
||||
|
||||
import { useChatApi } from "../useChatApi";
|
||||
|
||||
const axiosPostMock = vi.fn(() => ({}));
|
||||
const axiosGetMock = vi.fn(() => ({}));
|
||||
const axiosPutMock = vi.fn(() => ({}));
|
||||
const axiosDeleteMock = vi.fn(() => ({}));
|
||||
|
||||
vi.mock("@/lib/hooks", () => ({
|
||||
useAxios: () => ({
|
||||
axiosInstance: {
|
||||
post: axiosPostMock,
|
||||
get: axiosGetMock,
|
||||
put: axiosPutMock,
|
||||
delete: axiosDeleteMock,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("useChatApi", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should call createChat with the correct parameters", async () => {
|
||||
const chatName = "Test Chat";
|
||||
axiosPostMock.mockReturnValue({ data: {} });
|
||||
const {
|
||||
result: {
|
||||
current: { createChat },
|
||||
},
|
||||
} = renderHook(() => useChatApi());
|
||||
await createChat(chatName);
|
||||
|
||||
expect(axiosPostMock).toHaveBeenCalledTimes(1);
|
||||
expect(axiosPostMock).toHaveBeenCalledWith("/chat", {
|
||||
name: chatName,
|
||||
});
|
||||
});
|
||||
|
||||
it("should call getChats with the correct parameters", async () => {
|
||||
axiosGetMock.mockReturnValue({ data: {} });
|
||||
const {
|
||||
result: {
|
||||
current: { getChats },
|
||||
},
|
||||
} = renderHook(() => useChatApi());
|
||||
|
||||
await getChats();
|
||||
|
||||
expect(axiosGetMock).toHaveBeenCalledTimes(1);
|
||||
expect(axiosGetMock).toHaveBeenCalledWith("/chat");
|
||||
});
|
||||
|
||||
it("should call deleteChat with the correct parameters", async () => {
|
||||
const chatId = "test-chat-id";
|
||||
axiosDeleteMock.mockReturnValue({});
|
||||
const {
|
||||
result: {
|
||||
current: { deleteChat },
|
||||
},
|
||||
} = renderHook(() => useChatApi());
|
||||
|
||||
await deleteChat(chatId);
|
||||
|
||||
expect(axiosDeleteMock).toHaveBeenCalledTimes(1);
|
||||
expect(axiosDeleteMock).toHaveBeenCalledWith(`/chat/${chatId}`);
|
||||
});
|
||||
|
||||
it("should call addQuestion with the correct parameters", async () => {
|
||||
const chatId = "test-chat-id";
|
||||
|
||||
const chatQuestion: ChatQuestion = {
|
||||
question: "test-question",
|
||||
max_tokens: 10,
|
||||
model: "test-model",
|
||||
temperature: 0.5,
|
||||
};
|
||||
|
||||
const brainId = "test-brain-id";
|
||||
|
||||
const {
|
||||
result: {
|
||||
current: { addQuestion },
|
||||
},
|
||||
} = renderHook(() => useChatApi());
|
||||
|
||||
await addQuestion({ chatId, chatQuestion, brainId });
|
||||
|
||||
expect(axiosPostMock).toHaveBeenCalledTimes(1);
|
||||
expect(axiosPostMock).toHaveBeenCalledWith(
|
||||
`/chat/${chatId}/question?brain_id=${brainId}`,
|
||||
chatQuestion
|
||||
);
|
||||
});
|
||||
|
||||
it("should call getHistory with the correct parameters", async () => {
|
||||
const chatId = "test-chat-id";
|
||||
axiosGetMock.mockReturnValue({ data: {} });
|
||||
const {
|
||||
result: {
|
||||
current: { getChatItems: getHistory },
|
||||
},
|
||||
} = renderHook(() => useChatApi());
|
||||
|
||||
await getHistory(chatId);
|
||||
|
||||
expect(axiosGetMock).toHaveBeenCalledTimes(1);
|
||||
expect(axiosGetMock).toHaveBeenCalledWith(`/chat/${chatId}/history`);
|
||||
});
|
||||
|
||||
it("should call updateChat with the correct parameters", async () => {
|
||||
const chatId = "test-chat-id";
|
||||
const chatName = "test-chat-name";
|
||||
axiosPutMock.mockReturnValue({ data: {} });
|
||||
const {
|
||||
result: {
|
||||
current: { updateChat },
|
||||
},
|
||||
} = renderHook(() => useChatApi());
|
||||
|
||||
await updateChat(chatId, { chat_name: chatName });
|
||||
|
||||
expect(axiosPutMock).toHaveBeenCalledTimes(1);
|
||||
expect(axiosPutMock).toHaveBeenCalledWith(`/chat/${chatId}/metadata`, {
|
||||
chat_name: chatName,
|
||||
});
|
||||
});
|
||||
|
||||
it("should call addQuestionAndAnswer with the correct parameters", async () => {
|
||||
const chatId = "test-chat-id";
|
||||
const question = "test-question";
|
||||
const answer = "test-answer";
|
||||
axiosPostMock.mockReturnValue({ data: {} });
|
||||
const {
|
||||
result: {
|
||||
current: { addQuestionAndAnswer },
|
||||
},
|
||||
} = renderHook(() => useChatApi());
|
||||
|
||||
await addQuestionAndAnswer(chatId, { question, answer });
|
||||
|
||||
expect(axiosPostMock).toHaveBeenCalledTimes(1);
|
||||
expect(axiosPostMock).toHaveBeenCalledWith(
|
||||
`/chat/${chatId}/question/answer`,
|
||||
{ question, answer }
|
||||
);
|
||||
});
|
||||
});
|
@ -1,28 +0,0 @@
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { useNotificationApi } from "../useNotificationApi";
|
||||
|
||||
const axiosGetMock = vi.fn(() => ({}));
|
||||
|
||||
vi.mock("@/lib/hooks", () => ({
|
||||
useAxios: () => ({
|
||||
axiosInstance: {
|
||||
get: axiosGetMock,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("useNotificationApi", () => {
|
||||
it("should call getChatNotifications with the correct parameters", async () => {
|
||||
const chatId = "test-chat-id";
|
||||
const {
|
||||
result: {
|
||||
current: { getChatNotifications },
|
||||
},
|
||||
} = renderHook(() => useNotificationApi());
|
||||
await getChatNotifications(chatId);
|
||||
expect(axiosGetMock).toHaveBeenCalledTimes(1);
|
||||
expect(axiosGetMock).toHaveBeenCalledWith(`/notifications/${chatId}`);
|
||||
});
|
||||
});
|
110
frontend/lib/api/sync/sync.ts
Normal file
110
frontend/lib/api/sync/sync.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import { AxiosInstance } from "axios";
|
||||
import { UUID } from "crypto";
|
||||
|
||||
import {
|
||||
ActiveSync,
|
||||
OpenedConnection,
|
||||
Sync,
|
||||
SyncElement,
|
||||
SyncElements,
|
||||
} from "./types";
|
||||
|
||||
const createFilesSettings = (files: SyncElement[]) =>
|
||||
files.filter((file) => !file.is_folder).map((file) => file.id);
|
||||
|
||||
const createFoldersSettings = (files: SyncElement[]) =>
|
||||
files.filter((file) => file.is_folder).map((file) => file.id);
|
||||
|
||||
export const syncGoogleDrive = async (
|
||||
name: string,
|
||||
axiosInstance: AxiosInstance
|
||||
): Promise<{ authorization_url: string }> => {
|
||||
return (
|
||||
await axiosInstance.post<{ authorization_url: string }>(
|
||||
`/sync/google/authorize?name=${name}`
|
||||
)
|
||||
).data;
|
||||
};
|
||||
|
||||
export const syncSharepoint = async (
|
||||
name: string,
|
||||
axiosInstance: AxiosInstance
|
||||
): Promise<{ authorization_url: string }> => {
|
||||
return (
|
||||
await axiosInstance.post<{ authorization_url: string }>(
|
||||
`/sync/azure/authorize?name=${name}`
|
||||
)
|
||||
).data;
|
||||
};
|
||||
|
||||
export const getUserSyncs = async (
|
||||
axiosInstance: AxiosInstance
|
||||
): Promise<Sync[]> => {
|
||||
return (await axiosInstance.get<Sync[]>("/sync")).data;
|
||||
};
|
||||
|
||||
export const getSyncFiles = async (
|
||||
axiosInstance: AxiosInstance,
|
||||
userSyncId: number,
|
||||
folderId?: string
|
||||
): Promise<SyncElements> => {
|
||||
const url = folderId
|
||||
? `/sync/${userSyncId}/files?user_sync_id=${userSyncId}&folder_id=${folderId}`
|
||||
: `/sync/${userSyncId}/files?user_sync_id=${userSyncId}`;
|
||||
|
||||
return (await axiosInstance.get<SyncElements>(url)).data;
|
||||
};
|
||||
|
||||
export const syncFiles = async (
|
||||
axiosInstance: AxiosInstance,
|
||||
openedConnection: OpenedConnection,
|
||||
brainId: UUID
|
||||
): Promise<void> => {
|
||||
return (
|
||||
await axiosInstance.post<void>(`/sync/active`, {
|
||||
name: openedConnection.name,
|
||||
syncs_user_id: openedConnection.user_sync_id,
|
||||
settings: {
|
||||
files: createFilesSettings(openedConnection.selectedFiles.files),
|
||||
folders: createFoldersSettings(openedConnection.selectedFiles.files),
|
||||
},
|
||||
brain_id: brainId,
|
||||
})
|
||||
).data;
|
||||
};
|
||||
|
||||
export const updateActiveSync = async (
|
||||
axiosInstance: AxiosInstance,
|
||||
openedConnection: OpenedConnection
|
||||
): Promise<void> => {
|
||||
return (
|
||||
await axiosInstance.put<void>(`/sync/active/${openedConnection.id}`, {
|
||||
name: openedConnection.name,
|
||||
settings: {
|
||||
files: createFilesSettings(openedConnection.selectedFiles.files),
|
||||
folders: createFoldersSettings(openedConnection.selectedFiles.files),
|
||||
},
|
||||
last_synced: openedConnection.last_synced,
|
||||
})
|
||||
).data;
|
||||
};
|
||||
|
||||
export const deleteActiveSync = async (
|
||||
axiosInstance: AxiosInstance,
|
||||
syncId: number
|
||||
): Promise<void> => {
|
||||
await axiosInstance.delete<void>(`/sync/active/${syncId}`);
|
||||
};
|
||||
|
||||
export const getActiveSyncs = async (
|
||||
axiosInstance: AxiosInstance
|
||||
): Promise<ActiveSync[]> => {
|
||||
return (await axiosInstance.get<ActiveSync[]>(`/sync/active`)).data;
|
||||
};
|
||||
|
||||
export const deleteUserSync = async (
|
||||
axiosInstance: AxiosInstance,
|
||||
syncId: number
|
||||
): Promise<void> => {
|
||||
return (await axiosInstance.delete<void>(`/sync/${syncId}`)).data;
|
||||
};
|
53
frontend/lib/api/sync/types.ts
Normal file
53
frontend/lib/api/sync/types.ts
Normal file
@ -0,0 +1,53 @@
|
||||
export type Provider = "Google" | "Azure";
|
||||
|
||||
export interface SyncElement {
|
||||
name?: string;
|
||||
id: string;
|
||||
is_folder: boolean;
|
||||
}
|
||||
|
||||
export interface SyncElements {
|
||||
files: SyncElement[];
|
||||
}
|
||||
|
||||
interface Credentials {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface Sync {
|
||||
name: string;
|
||||
provider: Provider;
|
||||
id: number;
|
||||
credentials: Credentials;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface SyncSettings {
|
||||
folders?: string[];
|
||||
files?: string[];
|
||||
}
|
||||
|
||||
export interface ActiveSync {
|
||||
id: number;
|
||||
name: string;
|
||||
syncs_user_id: number;
|
||||
user_id: string;
|
||||
settings: SyncSettings;
|
||||
last_synced: string;
|
||||
sync_interval_minutes: number;
|
||||
brain_id: string;
|
||||
syncs_user: {
|
||||
provider: Provider;
|
||||
};
|
||||
}
|
||||
|
||||
export interface OpenedConnection {
|
||||
user_sync_id: number;
|
||||
id: number | undefined;
|
||||
provider: Provider;
|
||||
submitted: boolean;
|
||||
selectedFiles: SyncElements;
|
||||
name: string;
|
||||
last_synced: string;
|
||||
cleaned?: boolean;
|
||||
}
|
54
frontend/lib/api/sync/useSync.ts
Normal file
54
frontend/lib/api/sync/useSync.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { UUID } from "crypto";
|
||||
|
||||
import { useAxios } from "@/lib/hooks";
|
||||
|
||||
import {
|
||||
deleteActiveSync,
|
||||
deleteUserSync,
|
||||
getActiveSyncs,
|
||||
getSyncFiles,
|
||||
getUserSyncs,
|
||||
syncFiles,
|
||||
syncGoogleDrive,
|
||||
syncSharepoint,
|
||||
updateActiveSync,
|
||||
} from "./sync";
|
||||
import { OpenedConnection, Provider } from "./types";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useSync = () => {
|
||||
const { axiosInstance } = useAxios();
|
||||
|
||||
const iconUrls: Record<Provider, string> = {
|
||||
Google:
|
||||
"https://quivr-cms.s3.eu-west-3.amazonaws.com/gdrive_8316d080fd.png",
|
||||
Azure:
|
||||
"https://quivr-cms.s3.eu-west-3.amazonaws.com/sharepoint_8c41cfdb09.png",
|
||||
};
|
||||
|
||||
const getActiveSyncsForBrain = async (brainId: string) => {
|
||||
const activeSyncs = await getActiveSyncs(axiosInstance);
|
||||
|
||||
return activeSyncs.filter((sync) => sync.brain_id === brainId);
|
||||
};
|
||||
|
||||
return {
|
||||
syncGoogleDrive: async (name: string) =>
|
||||
syncGoogleDrive(name, axiosInstance),
|
||||
syncSharepoint: async (name: string) => syncSharepoint(name, axiosInstance),
|
||||
getUserSyncs: async () => getUserSyncs(axiosInstance),
|
||||
getSyncFiles: async (userSyncId: number, folderId?: string) =>
|
||||
getSyncFiles(axiosInstance, userSyncId, folderId),
|
||||
iconUrls,
|
||||
syncFiles: async (openedConnection: OpenedConnection, brainId: UUID) =>
|
||||
syncFiles(axiosInstance, openedConnection, brainId),
|
||||
getActiveSyncs: async () => getActiveSyncs(axiosInstance),
|
||||
getActiveSyncsForBrain,
|
||||
deleteUserSync: async (syncId: number) =>
|
||||
deleteUserSync(axiosInstance, syncId),
|
||||
deleteActiveSync: async (syncId: number) =>
|
||||
deleteActiveSync(axiosInstance, syncId),
|
||||
updateActiveSync: async (openedConnection: OpenedConnection) =>
|
||||
updateActiveSync(axiosInstance, openedConnection),
|
||||
};
|
||||
};
|
@ -5,9 +5,10 @@
|
||||
padding-block: Spacings.$spacing05;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 90%;
|
||||
min-height: 90%;
|
||||
overflow: hidden;
|
||||
gap: Spacings.$spacing08;
|
||||
gap: Spacings.$spacing05;
|
||||
|
||||
.stepper_container {
|
||||
width: 100%;
|
||||
@ -16,6 +17,6 @@
|
||||
|
||||
.content_wrapper {
|
||||
flex-grow: 1;
|
||||
overflow: scroll;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
@ -2,15 +2,17 @@ import { useEffect } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useFromConnectionsContext } from "@/app/chat/[chatId]/components/ActionsBar/components/KnowledgeToFeed/components/FromConnections/FromConnectionsProvider/hooks/useFromConnectionContext";
|
||||
import { Modal } from "@/lib/components/ui/Modal/Modal";
|
||||
import { addBrainDefaultValues } from "@/lib/config/defaultBrainConfig";
|
||||
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
|
||||
import { useUserData } from "@/lib/hooks/useUserData";
|
||||
|
||||
import styles from "./AddBrainModal.module.scss";
|
||||
import { useBrainCreationContext } from "./brainCreation-provider";
|
||||
import { BrainMainInfosStep } from "./components/BrainMainInfosStep/BrainMainInfosStep";
|
||||
import { BrainTypeSelectionStep } from "./components/BrainTypeSelectionStep/BrainTypeSelectionStep";
|
||||
import { CreateBrainStep } from "./components/CreateBrainStep/CreateBrainStep";
|
||||
import { BrainRecapStep } from "./components/BrainRecapStep/BrainRecapStep";
|
||||
import { FeedBrainStep } from "./components/FeedBrainStep/FeedBrainStep";
|
||||
import { Stepper } from "./components/Stepper/Stepper";
|
||||
import { useBrainCreationSteps } from "./hooks/useBrainCreationSteps";
|
||||
import { CreateBrainProps } from "./types/types";
|
||||
@ -19,12 +21,14 @@ export const AddBrainModal = (): JSX.Element => {
|
||||
const { t } = useTranslation(["translation", "brain", "config"]);
|
||||
const { userIdentityData } = useUserData();
|
||||
const { currentStep, steps } = useBrainCreationSteps();
|
||||
|
||||
const {
|
||||
isBrainCreationModalOpened,
|
||||
setIsBrainCreationModalOpened,
|
||||
setCurrentSelectedBrain,
|
||||
} = useBrainCreationContext();
|
||||
const { setCurrentSyncId, setOpenedConnections } =
|
||||
useFromConnectionsContext();
|
||||
const { removeAllKnowledgeToFeed } = useKnowledgeToFeedContext();
|
||||
|
||||
const defaultValues: CreateBrainProps = {
|
||||
...addBrainDefaultValues,
|
||||
@ -38,6 +42,10 @@ export const AddBrainModal = (): JSX.Element => {
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentSelectedBrain(undefined);
|
||||
setCurrentSyncId(undefined);
|
||||
setOpenedConnections([]);
|
||||
methods.reset(defaultValues);
|
||||
removeAllKnowledgeToFeed();
|
||||
}, [isBrainCreationModalOpened]);
|
||||
|
||||
return (
|
||||
@ -56,9 +64,9 @@ export const AddBrainModal = (): JSX.Element => {
|
||||
<Stepper currentStep={currentStep} steps={steps} />
|
||||
</div>
|
||||
<div className={styles.content_wrapper}>
|
||||
<BrainTypeSelectionStep />
|
||||
<BrainMainInfosStep />
|
||||
<CreateBrainStep />
|
||||
<FeedBrainStep />
|
||||
<BrainRecapStep />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
@ -1,3 +1,4 @@
|
||||
@use "@/styles/ScreenSizes.module.scss";
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
@use "@/styles/Typography.module.scss";
|
||||
|
||||
@ -7,6 +8,7 @@
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
padding-inline: Spacings.$spacing08;
|
||||
gap: Spacings.$spacing03;
|
||||
|
||||
.title {
|
||||
@include Typography.H2;
|
||||
@ -16,10 +18,21 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing05;
|
||||
overflow: scroll;
|
||||
|
||||
.name_field {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
@media (max-width: ScreenSizes.$small) {
|
||||
.name_field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttons_wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
@ -11,8 +11,7 @@ import styles from "./BrainMainInfosStep.module.scss";
|
||||
import { useBrainCreationSteps } from "../../hooks/useBrainCreationSteps";
|
||||
|
||||
export const BrainMainInfosStep = (): JSX.Element => {
|
||||
const { currentStepIndex, goToNextStep, goToPreviousStep } =
|
||||
useBrainCreationSteps();
|
||||
const { currentStepIndex, goToNextStep } = useBrainCreationSteps();
|
||||
|
||||
const { watch } = useFormContext<CreateBrainProps>();
|
||||
const name = watch("name");
|
||||
@ -24,11 +23,7 @@ export const BrainMainInfosStep = (): JSX.Element => {
|
||||
goToNextStep();
|
||||
};
|
||||
|
||||
const previous = (): void => {
|
||||
goToPreviousStep();
|
||||
};
|
||||
|
||||
if (currentStepIndex !== 1) {
|
||||
if (currentStepIndex !== 0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@ -36,7 +31,7 @@ export const BrainMainInfosStep = (): JSX.Element => {
|
||||
<div className={styles.brain_main_infos_wrapper}>
|
||||
<div className={styles.inputs_wrapper}>
|
||||
<span className={styles.title}>Define brain identity</span>
|
||||
<div>
|
||||
<div className={styles.name_field}>
|
||||
<FieldHeader iconName="brain" label="Name" mandatory={true} />
|
||||
<Controller
|
||||
name="name"
|
||||
@ -68,12 +63,6 @@ export const BrainMainInfosStep = (): JSX.Element => {
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.buttons_wrapper}>
|
||||
<QuivrButton
|
||||
color="primary"
|
||||
label="Previous Step"
|
||||
onClick={() => previous()}
|
||||
iconName="chevronLeft"
|
||||
/>
|
||||
<QuivrButton
|
||||
color="primary"
|
||||
label="Next Step"
|
||||
|
@ -0,0 +1,24 @@
|
||||
@use "@/styles/BoxShadow.module.scss";
|
||||
@use "@/styles/Radius.module.scss";
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
@use "@/styles/Typography.module.scss";
|
||||
|
||||
.brain_recap_card_wrapper {
|
||||
display: flex;
|
||||
padding: Spacings.$spacing05;
|
||||
justify-content: center;
|
||||
box-shadow: BoxShadow.$small;
|
||||
border-radius: Radius.$normal;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: Spacings.$spacing03;
|
||||
|
||||
.number_label {
|
||||
@include Typography.Big;
|
||||
color: var(--primary-0);
|
||||
}
|
||||
|
||||
.type {
|
||||
@include Typography.H1;
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import styles from "./BrainRecapCard.module.scss";
|
||||
|
||||
interface BrainRecapCardProps {
|
||||
label: string;
|
||||
number: number;
|
||||
}
|
||||
|
||||
export const BrainRecapCard = ({
|
||||
label,
|
||||
number,
|
||||
}: BrainRecapCardProps): JSX.Element => {
|
||||
return (
|
||||
<div className={styles.brain_recap_card_wrapper}>
|
||||
<span className={styles.number_label}>{number.toString()}</span>
|
||||
<span className={styles.type}>
|
||||
{label}
|
||||
{number > 1 ? "s" : ""}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,76 @@
|
||||
@use "@/styles/ScreenSizes.module.scss";
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
@use "@/styles/Typography.module.scss";
|
||||
|
||||
.brain_recap_wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding-inline: Spacings.$spacing08;
|
||||
gap: Spacings.$spacing03;
|
||||
overflow: hidden;
|
||||
|
||||
.content_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing05;
|
||||
overflow: scroll;
|
||||
|
||||
.title {
|
||||
@include Typography.H2;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
@include Typography.H3;
|
||||
}
|
||||
|
||||
.warning_message {
|
||||
font-size: Typography.$small;
|
||||
}
|
||||
|
||||
.brain_info_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing05;
|
||||
|
||||
.name_field {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: ScreenSizes.$small) {
|
||||
.name_field {
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cards_wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: Spacings.$spacing01;
|
||||
justify-content: space-between;
|
||||
gap: Spacings.$spacing05;
|
||||
|
||||
> * {
|
||||
min-width: 120px;
|
||||
max-width: 200px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@media screen and (max-width: ScreenSizes.$small) {
|
||||
> * {
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttons_wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
import { Controller } from "react-hook-form";
|
||||
|
||||
import { useFromConnectionsContext } from "@/app/chat/[chatId]/components/ActionsBar/components/KnowledgeToFeed/components/FromConnections/FromConnectionsProvider/hooks/useFromConnectionContext";
|
||||
import { useUserApi } from "@/lib/api/user/useUserApi";
|
||||
import { MessageInfoBox } from "@/lib/components/ui/MessageInfoBox/MessageInfoBox";
|
||||
import QuivrButton from "@/lib/components/ui/QuivrButton/QuivrButton";
|
||||
import { TextAreaInput } from "@/lib/components/ui/TextAreaInput/TextAreaInput";
|
||||
import { TextInput } from "@/lib/components/ui/TextInput/TextInput";
|
||||
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
|
||||
import { useUserData } from "@/lib/hooks/useUserData";
|
||||
|
||||
import { BrainRecapCard } from "./BrainRecapCard/BrainRecapCard";
|
||||
import styles from "./BrainRecapStep.module.scss";
|
||||
|
||||
import { useBrainCreationContext } from "../../brainCreation-provider";
|
||||
import { useBrainCreationSteps } from "../../hooks/useBrainCreationSteps";
|
||||
import { useBrainCreationApi } from "../FeedBrainStep/hooks/useBrainCreationApi";
|
||||
|
||||
export const BrainRecapStep = (): JSX.Element => {
|
||||
const { currentStepIndex, goToPreviousStep } = useBrainCreationSteps();
|
||||
const { creating, setCreating } = useBrainCreationContext();
|
||||
const { knowledgeToFeed } = useKnowledgeToFeedContext();
|
||||
const { createBrain } = useBrainCreationApi();
|
||||
const { updateUserIdentity } = useUserApi();
|
||||
const { userIdentityData } = useUserData();
|
||||
const { openedConnections, setOpenedConnections } =
|
||||
useFromConnectionsContext();
|
||||
|
||||
const feed = async (): Promise<void> => {
|
||||
if (!userIdentityData?.onboarded) {
|
||||
await updateUserIdentity({
|
||||
...userIdentityData,
|
||||
username: userIdentityData?.username ?? "",
|
||||
onboarded: true,
|
||||
});
|
||||
}
|
||||
setCreating(true);
|
||||
createBrain();
|
||||
};
|
||||
|
||||
const previous = (): void => {
|
||||
goToPreviousStep();
|
||||
};
|
||||
|
||||
if (currentStepIndex !== 2) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.brain_recap_wrapper}>
|
||||
<div className={styles.content_wrapper}>
|
||||
<span className={styles.title}>Brain Recap</span>
|
||||
<MessageInfoBox type="warning">
|
||||
<span className={styles.warning_message}>
|
||||
Depending on the number of knowledge, the upload can take
|
||||
<strong> few minutes</strong>.
|
||||
</span>
|
||||
</MessageInfoBox>
|
||||
<div className={styles.brain_info_wrapper}>
|
||||
<div className={styles.name_field}>
|
||||
<Controller
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
label="Enter your brain name"
|
||||
inputValue={field.value as string}
|
||||
setInputValue={field.onChange}
|
||||
disabled={true}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<TextAreaInput
|
||||
label="Enter your brain description"
|
||||
inputValue={field.value as string}
|
||||
setInputValue={field.onChange}
|
||||
disabled={true}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span className={styles.subtitle}>Knowledge From</span>
|
||||
<div className={styles.cards_wrapper}>
|
||||
<BrainRecapCard
|
||||
label="Connection"
|
||||
number={openedConnections.length}
|
||||
/>
|
||||
<BrainRecapCard
|
||||
label="URL"
|
||||
number={
|
||||
knowledgeToFeed.filter(
|
||||
(knowledge) => knowledge.source === "crawl"
|
||||
).length
|
||||
}
|
||||
/>
|
||||
<BrainRecapCard
|
||||
label="Document"
|
||||
number={
|
||||
knowledgeToFeed.filter(
|
||||
(knowledge) => knowledge.source === "upload"
|
||||
).length
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.buttons_wrapper}>
|
||||
<QuivrButton
|
||||
label="Previous step"
|
||||
color="primary"
|
||||
iconName="chevronLeft"
|
||||
onClick={previous}
|
||||
/>
|
||||
<QuivrButton
|
||||
label="Create"
|
||||
color="primary"
|
||||
iconName="add"
|
||||
onClick={async () => {
|
||||
await feed();
|
||||
setOpenedConnections([]);
|
||||
}}
|
||||
disabled={
|
||||
knowledgeToFeed.length === 0 && !userIdentityData?.onboarded
|
||||
}
|
||||
isLoading={creating}
|
||||
important={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,61 +0,0 @@
|
||||
import { IntegrationBrains } from "@/lib/api/brain/types";
|
||||
import { BrainCard } from "@/lib/components/ui/BrainCard/BrainCard";
|
||||
import { MessageInfoBox } from "@/lib/components/ui/MessageInfoBox/MessageInfoBox";
|
||||
import { useUserData } from "@/lib/hooks/useUserData";
|
||||
|
||||
import styles from "./BrainCatalogue.module.scss";
|
||||
|
||||
import { useBrainCreationContext } from "../../../brainCreation-provider";
|
||||
|
||||
export const BrainCatalogue = ({
|
||||
brains,
|
||||
next,
|
||||
}: {
|
||||
brains: IntegrationBrains[];
|
||||
next: () => void;
|
||||
}): JSX.Element => {
|
||||
const { setCurrentSelectedBrain, currentSelectedBrain } =
|
||||
useBrainCreationContext();
|
||||
const { userIdentityData } = useUserData();
|
||||
|
||||
return (
|
||||
<div className={styles.cards_wrapper}>
|
||||
<MessageInfoBox type="info">
|
||||
<span>
|
||||
A Brain is a specialized AI tool designed to interact with specific
|
||||
use cases or data sources.
|
||||
</span>
|
||||
</MessageInfoBox>
|
||||
{!userIdentityData?.onboarded && (
|
||||
<MessageInfoBox type="tutorial">
|
||||
<span>
|
||||
Let's start by creating a Docs & URLs brain.<br></br>Of
|
||||
course, feel free to explore other types of brains during your Quivr
|
||||
journey.
|
||||
</span>
|
||||
</MessageInfoBox>
|
||||
)}
|
||||
<span className={styles.title}>Choose a brain type</span>
|
||||
<div className={styles.brains_grid}>
|
||||
{brains.map((brain) => {
|
||||
return (
|
||||
<BrainCard
|
||||
key={brain.id}
|
||||
tooltip={brain.description}
|
||||
brainName={brain.integration_display_name}
|
||||
tags={brain.tags}
|
||||
selected={currentSelectedBrain?.id === brain.id}
|
||||
imageUrl={brain.integration_logo_url}
|
||||
callback={() => {
|
||||
next();
|
||||
setCurrentSelectedBrain(brain);
|
||||
}}
|
||||
cardKey={brain.id}
|
||||
disabled={!userIdentityData?.onboarded && !brain.onboarding_brain}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,42 +0,0 @@
|
||||
@use "@/styles/ScreenSizes.module.scss";
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
@use "@/styles/Typography.module.scss";
|
||||
|
||||
.brain_types_wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding-inline: Spacings.$spacing08;
|
||||
height: 100%;
|
||||
gap: Spacings.$spacing05;
|
||||
overflow-y: hidden;
|
||||
overflow-x: visible;
|
||||
|
||||
@media (max-width: ScreenSizes.$small) {
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.main_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing05;
|
||||
padding-top: Spacings.$spacing03;
|
||||
overflow-y: scroll;
|
||||
overflow-x: visible;
|
||||
|
||||
.title {
|
||||
@include Typography.H2;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons_wrapper {
|
||||
align-self: flex-end;
|
||||
|
||||
&.two_buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-self: normal;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { IntegrationBrains } from "@/lib/api/brain/types";
|
||||
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
|
||||
|
||||
import { BrainCatalogue } from "./BrainCatalogue/BrainCatalogue";
|
||||
import styles from "./BrainTypeSelectionStep.module.scss";
|
||||
|
||||
import { useBrainCreationSteps } from "../../hooks/useBrainCreationSteps";
|
||||
import { CreateBrainProps } from "../../types/types";
|
||||
|
||||
export const BrainTypeSelectionStep = (): JSX.Element => {
|
||||
const [brains, setBrains] = useState<IntegrationBrains[]>([]);
|
||||
const { goToNextStep, currentStepIndex } = useBrainCreationSteps();
|
||||
const { getIntegrationBrains } = useBrainApi();
|
||||
const { setValue } = useFormContext<CreateBrainProps>();
|
||||
|
||||
useEffect(() => {
|
||||
getIntegrationBrains()
|
||||
.then((response) => {
|
||||
setBrains(response);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
setValue("name", "");
|
||||
setValue("description", "");
|
||||
}, []);
|
||||
|
||||
if (currentStepIndex !== 0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.brain_types_wrapper}>
|
||||
<div className={styles.main_wrapper}>
|
||||
<BrainCatalogue brains={brains} next={goToNextStep} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,170 +0,0 @@
|
||||
import { capitalCase } from "change-case";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { KnowledgeToFeed } from "@/app/chat/[chatId]/components/ActionsBar/components";
|
||||
import { useUserApi } from "@/lib/api/user/useUserApi";
|
||||
import { MessageInfoBox } from "@/lib/components/ui/MessageInfoBox/MessageInfoBox";
|
||||
import QuivrButton from "@/lib/components/ui/QuivrButton/QuivrButton";
|
||||
import { TextInput } from "@/lib/components/ui/TextInput/TextInput";
|
||||
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
|
||||
import { useUserData } from "@/lib/hooks/useUserData";
|
||||
|
||||
import styles from "./CreateBrainStep.module.scss";
|
||||
import { useBrainCreationApi } from "./hooks/useBrainCreationApi";
|
||||
|
||||
import { useBrainCreationContext } from "../../brainCreation-provider";
|
||||
import { useBrainCreationSteps } from "../../hooks/useBrainCreationSteps";
|
||||
|
||||
export const CreateBrainStep = (): JSX.Element => {
|
||||
const { currentStepIndex, goToPreviousStep } = useBrainCreationSteps();
|
||||
const { createBrain, fields, setFields } = useBrainCreationApi();
|
||||
const { creating, setCreating, currentSelectedBrain } =
|
||||
useBrainCreationContext();
|
||||
const [createBrainStepIndex, setCreateBrainStepIndex] = useState<number>(0);
|
||||
const { knowledgeToFeed } = useKnowledgeToFeedContext();
|
||||
const { userIdentityData } = useUserData();
|
||||
const { updateUserIdentity } = useUserApi();
|
||||
|
||||
useEffect(() => {
|
||||
if (currentSelectedBrain?.connection_settings) {
|
||||
const newFields = Object.entries(
|
||||
currentSelectedBrain.connection_settings
|
||||
).map(([key, type]) => {
|
||||
return { name: key, type, value: "" };
|
||||
});
|
||||
setFields(newFields);
|
||||
}
|
||||
|
||||
setCreateBrainStepIndex(Number(!currentSelectedBrain?.connection_settings));
|
||||
}, [currentSelectedBrain?.connection_settings]);
|
||||
|
||||
const handleInputChange = (name: string, value: string) => {
|
||||
setFields(
|
||||
fields.map((field) => (field.name === name ? { ...field, value } : field))
|
||||
);
|
||||
};
|
||||
|
||||
const previous = (): void => {
|
||||
goToPreviousStep();
|
||||
};
|
||||
|
||||
const feed = async (): Promise<void> => {
|
||||
if (!userIdentityData?.onboarded) {
|
||||
await updateUserIdentity({
|
||||
...userIdentityData,
|
||||
username: userIdentityData?.username ?? "",
|
||||
onboarded: true,
|
||||
});
|
||||
}
|
||||
setCreating(true);
|
||||
createBrain();
|
||||
};
|
||||
|
||||
const renderSettings = () => {
|
||||
return (
|
||||
<>
|
||||
<MessageInfoBox type="warning">
|
||||
{currentSelectedBrain?.information}
|
||||
</MessageInfoBox>
|
||||
{fields.map(({ name, value }) => (
|
||||
<TextInput
|
||||
key={name}
|
||||
inputValue={value}
|
||||
setInputValue={(inputValue) => handleInputChange(name, inputValue)}
|
||||
label={capitalCase(name)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderFeedBrain = () => {
|
||||
return (
|
||||
<>
|
||||
{!userIdentityData?.onboarded && (
|
||||
<MessageInfoBox type="tutorial">
|
||||
<span>
|
||||
Upload documents or add URLs to add knowledges to your brain.
|
||||
</span>
|
||||
</MessageInfoBox>
|
||||
)}
|
||||
<div>
|
||||
<span className={styles.title}>Feed your brain</span>
|
||||
<KnowledgeToFeed hideBrainSelector={true} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderCreateButton = () => {
|
||||
return (
|
||||
<MessageInfoBox type="info">
|
||||
<div className={styles.message_content}>
|
||||
Click on
|
||||
<QuivrButton
|
||||
label="Create"
|
||||
color="primary"
|
||||
iconName="add"
|
||||
onClick={feed}
|
||||
isLoading={creating}
|
||||
/>
|
||||
to finish your brain creation.
|
||||
</div>
|
||||
</MessageInfoBox>
|
||||
);
|
||||
};
|
||||
|
||||
const renderButtons = () => {
|
||||
return (
|
||||
<div className={styles.buttons_wrapper}>
|
||||
<QuivrButton
|
||||
label="Previous step"
|
||||
color="primary"
|
||||
iconName="chevronLeft"
|
||||
onClick={previous}
|
||||
/>
|
||||
{(!currentSelectedBrain?.max_files && !createBrainStepIndex) ||
|
||||
createBrainStepIndex ? (
|
||||
<QuivrButton
|
||||
label="Create"
|
||||
color="primary"
|
||||
iconName="add"
|
||||
onClick={feed}
|
||||
disabled={
|
||||
knowledgeToFeed.length === 0 && !userIdentityData?.onboarded
|
||||
}
|
||||
isLoading={creating}
|
||||
important={true}
|
||||
/>
|
||||
) : (
|
||||
<QuivrButton
|
||||
label="Feed your brain"
|
||||
color="primary"
|
||||
iconName="add"
|
||||
onClick={() => setCreateBrainStepIndex(1)}
|
||||
isLoading={creating}
|
||||
important={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (currentStepIndex !== 2) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.brain_knowledge_wrapper}>
|
||||
{!createBrainStepIndex && renderSettings()}
|
||||
{!!currentSelectedBrain?.max_files &&
|
||||
!!createBrainStepIndex &&
|
||||
renderFeedBrain()}
|
||||
{!currentSelectedBrain?.max_files &&
|
||||
!currentSelectedBrain?.connection_settings &&
|
||||
renderCreateButton()}
|
||||
|
||||
{renderButtons()}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -8,14 +8,15 @@
|
||||
padding-inline: Spacings.$spacing08;
|
||||
height: 100%;
|
||||
|
||||
.title {
|
||||
@include Typography.H2;
|
||||
}
|
||||
|
||||
.settings_wrapper {
|
||||
.feed_brain {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing05;
|
||||
overflow: scroll;
|
||||
height: 100%;
|
||||
|
||||
.title {
|
||||
@include Typography.H2;
|
||||
}
|
||||
}
|
||||
|
||||
.message_info_box_wrapper {
|
@ -0,0 +1,113 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { KnowledgeToFeed } from "@/app/chat/[chatId]/components/ActionsBar/components";
|
||||
import { useFromConnectionsContext } from "@/app/chat/[chatId]/components/ActionsBar/components/KnowledgeToFeed/components/FromConnections/FromConnectionsProvider/hooks/useFromConnectionContext";
|
||||
import { OpenedConnection } from "@/lib/api/sync/types";
|
||||
import { MessageInfoBox } from "@/lib/components/ui/MessageInfoBox/MessageInfoBox";
|
||||
import QuivrButton from "@/lib/components/ui/QuivrButton/QuivrButton";
|
||||
import { createHandleGetButtonProps } from "@/lib/helpers/handleConnectionButtons";
|
||||
import { useUserData } from "@/lib/hooks/useUserData";
|
||||
|
||||
import styles from "./FeedBrainStep.module.scss";
|
||||
|
||||
import { useBrainCreationSteps } from "../../hooks/useBrainCreationSteps";
|
||||
|
||||
export const FeedBrainStep = (): JSX.Element => {
|
||||
const { currentStepIndex, goToPreviousStep, goToNextStep } =
|
||||
useBrainCreationSteps();
|
||||
const { userIdentityData } = useUserData();
|
||||
const {
|
||||
currentSyncId,
|
||||
setCurrentSyncId,
|
||||
openedConnections,
|
||||
setOpenedConnections,
|
||||
} = useFromConnectionsContext();
|
||||
const [currentConnection, setCurrentConnection] = useState<
|
||||
OpenedConnection | undefined
|
||||
>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentConnection(
|
||||
openedConnections.find(
|
||||
(connection) => connection.user_sync_id === currentSyncId
|
||||
)
|
||||
);
|
||||
}, [currentSyncId]);
|
||||
|
||||
const getButtonProps = createHandleGetButtonProps(
|
||||
currentConnection,
|
||||
openedConnections,
|
||||
setOpenedConnections,
|
||||
currentSyncId,
|
||||
setCurrentSyncId
|
||||
);
|
||||
|
||||
const renderFeedBrain = () => (
|
||||
<>
|
||||
{!userIdentityData?.onboarded && (
|
||||
<MessageInfoBox type="tutorial">
|
||||
<span>
|
||||
Upload documents or add URLs to add knowledges to your brain.
|
||||
</span>
|
||||
</MessageInfoBox>
|
||||
)}
|
||||
<div className={styles.feed_brain}>
|
||||
<span className={styles.title}>Feed your brain</span>
|
||||
<KnowledgeToFeed hideBrainSelector={true} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const renderButtons = () => {
|
||||
const buttonProps = getButtonProps();
|
||||
|
||||
return (
|
||||
<div className={styles.buttons_wrapper}>
|
||||
{currentSyncId ? (
|
||||
<QuivrButton
|
||||
label="Back to connections"
|
||||
color="primary"
|
||||
iconName="chevronLeft"
|
||||
onClick={() => setCurrentSyncId(undefined)}
|
||||
/>
|
||||
) : (
|
||||
<QuivrButton
|
||||
label="Previous step"
|
||||
color="primary"
|
||||
iconName="chevronLeft"
|
||||
onClick={goToPreviousStep}
|
||||
/>
|
||||
)}
|
||||
{currentSyncId ? (
|
||||
<QuivrButton
|
||||
label={buttonProps.label}
|
||||
color={buttonProps.type}
|
||||
iconName={buttonProps.type === "dangerous" ? "delete" : "add"}
|
||||
onClick={buttonProps.callback}
|
||||
important={true}
|
||||
disabled={buttonProps.disabled}
|
||||
/>
|
||||
) : (
|
||||
<QuivrButton
|
||||
label={"Next step"}
|
||||
color="primary"
|
||||
iconName="chevronRight"
|
||||
onClick={goToNextStep}
|
||||
important={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (currentStepIndex !== 1) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.brain_knowledge_wrapper}>
|
||||
{renderFeedBrain()}
|
||||
{renderButtons()}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -5,8 +5,10 @@ import { useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useFromConnectionsContext } from "@/app/chat/[chatId]/components/ActionsBar/components/KnowledgeToFeed/components/FromConnections/FromConnectionsProvider/hooks/useFromConnectionContext";
|
||||
import { PUBLIC_BRAINS_KEY } from "@/lib/api/brain/config";
|
||||
import { IntegrationSettings } from "@/lib/api/brain/types";
|
||||
import { useSync } from "@/lib/api/sync/useSync";
|
||||
import { CreateBrainProps } from "@/lib/components/AddBrainModal/types/types";
|
||||
import { useKnowledgeToFeedInput } from "@/lib/components/KnowledgeToFeedInput/hooks/useKnowledgeToFeedInput.ts";
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
@ -31,14 +33,22 @@ export const useBrainCreationApi = () => {
|
||||
const [fields, setFields] = useState<
|
||||
{ name: string; type: string; value: string }[]
|
||||
>([]);
|
||||
const { syncFiles } = useSync();
|
||||
const { openedConnections } = useFromConnectionsContext();
|
||||
|
||||
const handleFeedBrain = async (brainId: UUID): Promise<void> => {
|
||||
const uploadPromises = files.map((file) =>
|
||||
uploadFileHandler(file, brainId)
|
||||
);
|
||||
|
||||
const crawlPromises = urls.map((url) => crawlWebsiteHandler(url, brainId));
|
||||
|
||||
await Promise.all([...uploadPromises, ...crawlPromises]);
|
||||
await Promise.all(
|
||||
openedConnections.map(async (openedConnection) => {
|
||||
await syncFiles(openedConnection, brainId);
|
||||
})
|
||||
);
|
||||
setKnowledgeToFeed([]);
|
||||
};
|
||||
|
@ -14,8 +14,8 @@
|
||||
position: relative;
|
||||
|
||||
.circle {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
background-color: var(--primary-0);
|
||||
border-radius: Radius.$circle;
|
||||
display: flex;
|
||||
@ -80,7 +80,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: Typography.$tiny;
|
||||
width: 2.5rem;
|
||||
width: 1.75rem;
|
||||
|
||||
.step_index {
|
||||
white-space: nowrap;
|
||||
@ -95,7 +95,7 @@
|
||||
border-radius: Radius.$big;
|
||||
background-color: var(--primary-1);
|
||||
margin: 0 8px;
|
||||
margin-top: Spacings.$spacing05;
|
||||
margin-top: Spacings.$spacing04;
|
||||
|
||||
&.done {
|
||||
background-color: var(--success);
|
||||
|
@ -0,0 +1,13 @@
|
||||
@use "@/styles/ScreenSizes.module.scss";
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
|
||||
.connection_cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: Spacings.$spacing05;
|
||||
width: 100%;
|
||||
|
||||
&.spaced {
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
35
frontend/lib/components/ConnectionCards/ConnectionCards.tsx
Normal file
35
frontend/lib/components/ConnectionCards/ConnectionCards.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { useSync } from "@/lib/api/sync/useSync";
|
||||
|
||||
import styles from "./ConnectionCards.module.scss";
|
||||
import { ConnectionSection } from "./ConnectionSection/ConnectionSection";
|
||||
|
||||
interface ConnectionCardsProps {
|
||||
fromAddKnowledge?: boolean;
|
||||
}
|
||||
|
||||
export const ConnectionCards = ({
|
||||
fromAddKnowledge,
|
||||
}: ConnectionCardsProps): JSX.Element => {
|
||||
const { syncGoogleDrive, syncSharepoint } = useSync();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.connection_cards} ${
|
||||
fromAddKnowledge ? styles.spaced : ""
|
||||
}`}
|
||||
>
|
||||
<ConnectionSection
|
||||
label="Google Drive"
|
||||
provider="Google"
|
||||
callback={(name) => syncGoogleDrive(name)}
|
||||
fromAddKnowledge={fromAddKnowledge}
|
||||
/>
|
||||
<ConnectionSection
|
||||
label="Sharepoint"
|
||||
provider="Azure"
|
||||
callback={(name) => syncSharepoint(name)}
|
||||
fromAddKnowledge={fromAddKnowledge}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,26 @@
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
@use "@/styles/Typography.module.scss";
|
||||
|
||||
.connection_button_wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: Spacings.$spacing03;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
gap: Spacings.$spacing02;
|
||||
overflow: hidden;
|
||||
|
||||
.label {
|
||||
@include Typography.EllipsisOverflow;
|
||||
font-size: Typography.$small;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons_wrapper {
|
||||
display: flex;
|
||||
gap: Spacings.$spacing02;
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import { ConnectionIcon } from "@/lib/components/ui/ConnectionIcon/ConnectionIcon";
|
||||
import QuivrButton from "@/lib/components/ui/QuivrButton/QuivrButton";
|
||||
|
||||
import styles from "./ConnectionButton.module.scss";
|
||||
|
||||
interface ConnectionButtonProps {
|
||||
label: string;
|
||||
index: number;
|
||||
onClick: (id: number) => void;
|
||||
submitted?: boolean;
|
||||
}
|
||||
|
||||
export const ConnectionButton = ({
|
||||
label,
|
||||
index,
|
||||
onClick,
|
||||
submitted,
|
||||
}: ConnectionButtonProps): JSX.Element => {
|
||||
return (
|
||||
<div className={styles.connection_button_wrapper}>
|
||||
<div className={styles.left}>
|
||||
<ConnectionIcon letter={label[0]} index={index} />
|
||||
<span className={styles.label}>{label}</span>
|
||||
</div>
|
||||
<div className={styles.buttons_wrapper}>
|
||||
<QuivrButton
|
||||
label={submitted ? "Update" : "Use"}
|
||||
small={true}
|
||||
iconName="chevronRight"
|
||||
color="primary"
|
||||
onClick={() => onClick(index)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,25 @@
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
@use "@/styles/Typography.module.scss";
|
||||
|
||||
.connection_line_wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
gap: Spacings.$spacing02;
|
||||
overflow: hidden;
|
||||
|
||||
.label {
|
||||
@include Typography.EllipsisOverflow;
|
||||
font-size: Typography.$small;
|
||||
}
|
||||
}
|
||||
|
||||
.icons {
|
||||
display: flex;
|
||||
gap: Spacings.$spacing02;
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import { useFromConnectionsContext } from "@/app/chat/[chatId]/components/ActionsBar/components/KnowledgeToFeed/components/FromConnections/FromConnectionsProvider/hooks/useFromConnectionContext";
|
||||
import { useSync } from "@/lib/api/sync/useSync";
|
||||
import { ConnectionIcon } from "@/lib/components/ui/ConnectionIcon/ConnectionIcon";
|
||||
import Icon from "@/lib/components/ui/Icon/Icon";
|
||||
|
||||
import styles from "./ConnectionLine.module.scss";
|
||||
|
||||
interface ConnectionLineProps {
|
||||
label: string;
|
||||
index: number;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export const ConnectionLine = ({
|
||||
label,
|
||||
index,
|
||||
id,
|
||||
}: ConnectionLineProps): JSX.Element => {
|
||||
const { deleteUserSync } = useSync();
|
||||
const { setHasToReload } = useFromConnectionsContext();
|
||||
|
||||
return (
|
||||
<div className={styles.connection_line_wrapper}>
|
||||
<div className={styles.left}>
|
||||
<ConnectionIcon letter={label[0]} index={index} />
|
||||
<span className={styles.label}>{label}</span>
|
||||
</div>
|
||||
<div className={styles.icons}>
|
||||
<Icon
|
||||
name="delete"
|
||||
size="normal"
|
||||
color="dangerous"
|
||||
handleHover={true}
|
||||
onClick={async () => {
|
||||
await deleteUserSync(id);
|
||||
setHasToReload(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,84 @@
|
||||
@use "@/styles/BoxShadow.module.scss";
|
||||
@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";
|
||||
|
||||
.connection_section_wrapper {
|
||||
padding: Spacings.$spacing05;
|
||||
border-radius: Radius.$normal;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid transparent;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: Radius.$normal;
|
||||
box-shadow: BoxShadow.$medium;
|
||||
height: min-content;
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: ScreenSizes.$small) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.connection_section_header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding-block: Spacings.$spacing03;
|
||||
@include Typography.H3;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
gap: Spacings.$spacing03;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.iconRotate {
|
||||
transition: transform 0.3s Transitions.$easeOutBack;
|
||||
}
|
||||
|
||||
.iconRotateDown {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
.iconRotateRight {
|
||||
transform: rotate(-90deg);
|
||||
|
||||
.label {
|
||||
@include Typography.H3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.existing_connections {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing03;
|
||||
width: 100%;
|
||||
padding-top: Spacings.$spacing05;
|
||||
|
||||
.existing_connections_header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.label {
|
||||
font-weight: 500;
|
||||
font-size: Typography.$tiny;
|
||||
}
|
||||
}
|
||||
|
||||
.folded {
|
||||
display: flex;
|
||||
padding-left: Spacings.$spacing03;
|
||||
|
||||
.negative_margin {
|
||||
margin-left: -(Spacings.$spacing02);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,255 @@
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { useFromConnectionsContext } from "@/app/chat/[chatId]/components/ActionsBar/components/KnowledgeToFeed/components/FromConnections/FromConnectionsProvider/hooks/useFromConnectionContext";
|
||||
import { OpenedConnection, Provider, Sync } from "@/lib/api/sync/types";
|
||||
import { useSync } from "@/lib/api/sync/useSync";
|
||||
import QuivrButton from "@/lib/components/ui/QuivrButton/QuivrButton";
|
||||
|
||||
import { ConnectionButton } from "./ConnectionButton/ConnectionButton";
|
||||
import { ConnectionLine } from "./ConnectionLine/ConnectionLine";
|
||||
import styles from "./ConnectionSection.module.scss";
|
||||
|
||||
import { ConnectionIcon } from "../../ui/ConnectionIcon/ConnectionIcon";
|
||||
import { Icon } from "../../ui/Icon/Icon";
|
||||
import { TextButton } from "../../ui/TextButton/TextButton";
|
||||
|
||||
interface ConnectionSectionProps {
|
||||
label: string;
|
||||
provider: Provider;
|
||||
callback: (name: string) => Promise<{ authorization_url: string }>;
|
||||
fromAddKnowledge?: boolean;
|
||||
}
|
||||
|
||||
const renderConnectionLines = (
|
||||
existingConnections: Sync[],
|
||||
folded: boolean
|
||||
) => {
|
||||
if (!folded) {
|
||||
return existingConnections.map((connection, index) => (
|
||||
<div key={index}>
|
||||
<ConnectionLine
|
||||
label={connection.email}
|
||||
index={index}
|
||||
id={connection.id}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
} else {
|
||||
return (
|
||||
<div className={styles.folded}>
|
||||
{existingConnections.map((connection, index) => (
|
||||
<div className={styles.negative_margin} key={index}>
|
||||
<ConnectionIcon letter={connection.email[0]} index={index} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderExistingConnections = ({
|
||||
existingConnections,
|
||||
folded,
|
||||
setFolded,
|
||||
fromAddKnowledge,
|
||||
handleGetSyncFiles,
|
||||
openedConnections,
|
||||
}: {
|
||||
existingConnections: Sync[];
|
||||
folded: boolean;
|
||||
setFolded: (folded: boolean) => void;
|
||||
fromAddKnowledge: boolean;
|
||||
handleGetSyncFiles: (
|
||||
userSyncId: number,
|
||||
currentProvider: Provider
|
||||
) => Promise<void>;
|
||||
openedConnections: OpenedConnection[];
|
||||
}) => {
|
||||
if (!!existingConnections.length && !fromAddKnowledge) {
|
||||
return (
|
||||
<div className={styles.existing_connections}>
|
||||
<div className={styles.existing_connections_header}>
|
||||
<span className={styles.label}>Connected accounts</span>
|
||||
<Icon
|
||||
name="settings"
|
||||
size="normal"
|
||||
color="black"
|
||||
handleHover={true}
|
||||
onClick={() => setFolded(!folded)}
|
||||
/>
|
||||
</div>
|
||||
{renderConnectionLines(existingConnections, folded)}
|
||||
</div>
|
||||
);
|
||||
} else if (existingConnections.length > 0 && fromAddKnowledge) {
|
||||
return (
|
||||
<div className={styles.existing_connections}>
|
||||
{existingConnections.map((connection, index) => (
|
||||
<div key={index}>
|
||||
<ConnectionButton
|
||||
label={connection.email}
|
||||
index={index}
|
||||
submitted={openedConnections.some((openedConnection) => {
|
||||
return (
|
||||
openedConnection.name === connection.name &&
|
||||
openedConnection.submitted
|
||||
);
|
||||
})}
|
||||
onClick={() =>
|
||||
void handleGetSyncFiles(connection.id, connection.provider)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const ConnectionSection = ({
|
||||
label,
|
||||
provider,
|
||||
fromAddKnowledge,
|
||||
callback,
|
||||
}: ConnectionSectionProps): JSX.Element => {
|
||||
const { iconUrls, getUserSyncs, getSyncFiles } = useSync();
|
||||
const {
|
||||
setCurrentSyncElements,
|
||||
setCurrentSyncId,
|
||||
setOpenedConnections,
|
||||
openedConnections,
|
||||
hasToReload,
|
||||
setHasToReload,
|
||||
} = useFromConnectionsContext();
|
||||
const [existingConnections, setExistingConnections] = useState<Sync[]>([]);
|
||||
const [folded, setFolded] = useState<boolean>(!fromAddKnowledge);
|
||||
|
||||
const fetchUserSyncs = async () => {
|
||||
try {
|
||||
const res: Sync[] = await getUserSyncs();
|
||||
setExistingConnections(
|
||||
res.filter(
|
||||
(sync) =>
|
||||
Object.keys(sync.credentials).length !== 0 &&
|
||||
sync.provider === provider
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void fetchUserSyncs();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === "visible" && !document.hidden) {
|
||||
void fetchUserSyncs();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasToReload) {
|
||||
void fetchUserSyncs();
|
||||
setHasToReload(false);
|
||||
}
|
||||
}, [hasToReload]);
|
||||
|
||||
const handleOpenedConnections = (userSyncId: number) => {
|
||||
const existingConnection = openedConnections.find(
|
||||
(connection) => connection.user_sync_id === userSyncId
|
||||
);
|
||||
|
||||
if (!existingConnection) {
|
||||
const newConnection: OpenedConnection = {
|
||||
name:
|
||||
existingConnections.find((connection) => connection.id === userSyncId)
|
||||
?.name ?? "",
|
||||
user_sync_id: userSyncId,
|
||||
id: undefined,
|
||||
provider: provider,
|
||||
submitted: false,
|
||||
selectedFiles: { files: [] },
|
||||
last_synced: "",
|
||||
};
|
||||
|
||||
setOpenedConnections([...openedConnections, newConnection]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGetSyncFiles = async (userSyncId: number) => {
|
||||
try {
|
||||
const res = await getSyncFiles(userSyncId);
|
||||
setCurrentSyncElements(res);
|
||||
setCurrentSyncId(userSyncId);
|
||||
handleOpenedConnections(userSyncId);
|
||||
} catch (error) {
|
||||
console.error("Failed to get sync files:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const connect = async () => {
|
||||
const res = await callback(
|
||||
Math.random().toString(36).substring(2, 15) +
|
||||
Math.random().toString(36).substring(2, 15)
|
||||
);
|
||||
if (res.authorization_url) {
|
||||
window.open(res.authorization_url, "_blank");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.connection_section_wrapper}>
|
||||
<div className={styles.connection_section_header}>
|
||||
<div className={styles.left}>
|
||||
<Image
|
||||
src={iconUrls[provider]}
|
||||
alt={label}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
<span className={styles.label}>{label}</span>
|
||||
</div>
|
||||
{!fromAddKnowledge ? (
|
||||
<QuivrButton
|
||||
iconName={existingConnections.length ? "add" : "sync"}
|
||||
label={existingConnections.length ? "Add more" : "Connect"}
|
||||
color="primary"
|
||||
onClick={() => connect()}
|
||||
small={true}
|
||||
/>
|
||||
) : (
|
||||
<TextButton
|
||||
iconName={existingConnections.length ? "add" : "sync"}
|
||||
label={existingConnections.length ? "Add more" : "Connect"}
|
||||
color="black"
|
||||
onClick={() => connect()}
|
||||
small={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{renderExistingConnections({
|
||||
existingConnections,
|
||||
folded,
|
||||
setFolded,
|
||||
fromAddKnowledge: !!fromAddKnowledge,
|
||||
handleGetSyncFiles,
|
||||
openedConnections,
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
@ -53,11 +53,18 @@ export const PageHeader = ({
|
||||
/>
|
||||
))}
|
||||
{!isMobile && <Notifications />}
|
||||
<Icon
|
||||
name="settings"
|
||||
color="black"
|
||||
handleHover={true}
|
||||
size="normal"
|
||||
onClick={() => void (window.location.href = "/user")}
|
||||
/>
|
||||
<Icon
|
||||
name={lightModeIconName}
|
||||
color="black"
|
||||
handleHover={true}
|
||||
size="small"
|
||||
size="normal"
|
||||
onClick={toggleTheme}
|
||||
/>
|
||||
</div>
|
||||
|
@ -10,8 +10,12 @@
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
|
||||
.button {
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
justify-content: space-between;
|
||||
|
||||
&.standalone {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { KnowledgeToFeed } from "@/app/chat/[chatId]/components/ActionsBar/components";
|
||||
import { useFromConnectionsContext } from "@/app/chat/[chatId]/components/ActionsBar/components/KnowledgeToFeed/components/FromConnections/FromConnectionsProvider/hooks/useFromConnectionContext";
|
||||
import { OpenedConnection } from "@/lib/api/sync/types";
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
|
||||
import { createHandleGetButtonProps } from "@/lib/helpers/handleConnectionButtons";
|
||||
|
||||
import styles from "./UploadDocumentModal.module.scss";
|
||||
import { useAddKnowledge } from "./hooks/useAddKnowledge";
|
||||
@ -17,10 +20,29 @@ export const UploadDocumentModal = (): JSX.Element => {
|
||||
const { currentBrain } = useBrainContext();
|
||||
const { feedBrain } = useAddKnowledge();
|
||||
const [feeding, setFeeding] = useState<boolean>(false);
|
||||
const {
|
||||
currentSyncId,
|
||||
setCurrentSyncId,
|
||||
openedConnections,
|
||||
setOpenedConnections,
|
||||
} = useFromConnectionsContext();
|
||||
const [currentConnection, setCurrentConnection] = useState<
|
||||
OpenedConnection | undefined
|
||||
>(undefined);
|
||||
|
||||
useKnowledgeToFeedContext();
|
||||
const { t } = useTranslation(["knowledge"]);
|
||||
|
||||
const disabled = useMemo(() => {
|
||||
return (
|
||||
(knowledgeToFeed.length === 0 &&
|
||||
openedConnections.filter((connection) => {
|
||||
return connection.submitted || !!connection.last_synced;
|
||||
}).length === 0) ||
|
||||
!currentBrain
|
||||
);
|
||||
}, [knowledgeToFeed, openedConnections, currentBrain, currentSyncId]);
|
||||
|
||||
const handleFeedBrain = async () => {
|
||||
setFeeding(true);
|
||||
await feedBrain();
|
||||
@ -28,6 +50,23 @@ export const UploadDocumentModal = (): JSX.Element => {
|
||||
setShouldDisplayFeedCard(false);
|
||||
};
|
||||
|
||||
const getButtonProps = createHandleGetButtonProps(
|
||||
currentConnection,
|
||||
openedConnections,
|
||||
setOpenedConnections,
|
||||
currentSyncId,
|
||||
setCurrentSyncId
|
||||
);
|
||||
const buttonProps = getButtonProps();
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentConnection(
|
||||
openedConnections.find(
|
||||
(connection) => connection.user_sync_id === currentSyncId
|
||||
)
|
||||
);
|
||||
}, [currentSyncId]);
|
||||
|
||||
if (!shouldDisplayFeedCard) {
|
||||
return <></>;
|
||||
}
|
||||
@ -43,16 +82,44 @@ export const UploadDocumentModal = (): JSX.Element => {
|
||||
>
|
||||
<div className={styles.knowledge_modal}>
|
||||
<KnowledgeToFeed />
|
||||
<div className={styles.button}>
|
||||
<QuivrButton
|
||||
label="Feed Brain"
|
||||
color="primary"
|
||||
iconName="add"
|
||||
onClick={handleFeedBrain}
|
||||
disabled={knowledgeToFeed.length === 0 || !currentBrain}
|
||||
isLoading={feeding}
|
||||
important={true}
|
||||
/>
|
||||
<div
|
||||
className={`${styles.buttons} ${
|
||||
!currentSyncId ? styles.standalone : ""
|
||||
}`}
|
||||
>
|
||||
{!!currentSyncId && (
|
||||
<QuivrButton
|
||||
label="Back to connections"
|
||||
color="primary"
|
||||
iconName="chevronLeft"
|
||||
onClick={() => {
|
||||
setCurrentSyncId(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{currentSyncId ? (
|
||||
<QuivrButton
|
||||
label={buttonProps.label}
|
||||
color={buttonProps.type}
|
||||
iconName={buttonProps.type === "dangerous" ? "delete" : "add"}
|
||||
onClick={buttonProps.callback}
|
||||
important={true}
|
||||
disabled={buttonProps.disabled}
|
||||
/>
|
||||
) : (
|
||||
<QuivrButton
|
||||
label="Feed Brain"
|
||||
color="primary"
|
||||
iconName="add"
|
||||
onClick={() => {
|
||||
setOpenedConnections([]);
|
||||
void handleFeedBrain();
|
||||
}}
|
||||
disabled={disabled}
|
||||
isLoading={feeding}
|
||||
important={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useFromConnectionsContext } from "@/app/chat/[chatId]/components/ActionsBar/components/KnowledgeToFeed/components/FromConnections/FromConnectionsProvider/hooks/useFromConnectionContext";
|
||||
import { useChatApi } from "@/lib/api/chat/useChatApi";
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
|
||||
@ -25,6 +26,7 @@ export const useFeedBrain = ({
|
||||
useKnowledgeToFeedContext();
|
||||
const [hasPendingRequests, setHasPendingRequests] = useState(false);
|
||||
const { handleFeedBrain } = useFeedBrainHandler();
|
||||
const { openedConnections } = useFromConnectionsContext();
|
||||
|
||||
const { createChat, deleteChat } = useChatApi();
|
||||
|
||||
@ -39,7 +41,7 @@ export const useFeedBrain = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (knowledgeToFeed.length === 0) {
|
||||
if (knowledgeToFeed.length === 0 && !openedConnections.length) {
|
||||
publish({
|
||||
variant: "danger",
|
||||
text: t("addFiles"),
|
||||
@ -55,12 +57,11 @@ export const useFeedBrain = ({
|
||||
dispatchHasPendingRequests?.();
|
||||
closeFeedInput?.();
|
||||
setHasPendingRequests(true);
|
||||
setShouldDisplayFeedCard(false);
|
||||
await handleFeedBrain({
|
||||
brainId,
|
||||
chatId: currentChatId,
|
||||
});
|
||||
|
||||
setShouldDisplayFeedCard(false);
|
||||
setKnowledgeToFeed([]);
|
||||
} catch (e) {
|
||||
publish({
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { UUID } from "crypto";
|
||||
|
||||
import { useFromConnectionsContext } from "@/app/chat/[chatId]/components/ActionsBar/components/KnowledgeToFeed/components/FromConnections/FromConnectionsProvider/hooks/useFromConnectionContext";
|
||||
import { useSync } from "@/lib/api/sync/useSync";
|
||||
import { useKnowledgeToFeedInput } from "@/lib/components/KnowledgeToFeedInput/hooks/useKnowledgeToFeedInput.ts";
|
||||
import { useKnowledgeToFeedFilesAndUrls } from "@/lib/hooks/useKnowledgeToFeed";
|
||||
import { useOnboarding } from "@/lib/hooks/useOnboarding";
|
||||
@ -13,6 +15,13 @@ export const useFeedBrainHandler = () => {
|
||||
const { files, urls } = useKnowledgeToFeedFilesAndUrls();
|
||||
const { crawlWebsiteHandler, uploadFileHandler } = useKnowledgeToFeedInput();
|
||||
const { updateOnboarding, onboarding } = useOnboarding();
|
||||
const {
|
||||
syncFiles,
|
||||
getActiveSyncsForBrain,
|
||||
deleteActiveSync,
|
||||
updateActiveSync,
|
||||
} = useSync();
|
||||
const { openedConnections } = useFromConnectionsContext();
|
||||
|
||||
const updateOnboardingA = async () => {
|
||||
if (onboarding.onboarding_a) {
|
||||
@ -33,6 +42,26 @@ export const useFeedBrainHandler = () => {
|
||||
crawlWebsiteHandler(url, brainId, chatId)
|
||||
);
|
||||
|
||||
const existingConnections = await getActiveSyncsForBrain(brainId);
|
||||
|
||||
await Promise.all(
|
||||
openedConnections.map(async (openedConnection) => {
|
||||
const existingConnectionIds = existingConnections.map(
|
||||
(connection) => connection.id
|
||||
);
|
||||
if (
|
||||
!openedConnection.id ||
|
||||
!existingConnectionIds.includes(openedConnection.id)
|
||||
) {
|
||||
await syncFiles(openedConnection, brainId);
|
||||
} else if (!openedConnection.selectedFiles.files.length) {
|
||||
await deleteActiveSync(openedConnection.id);
|
||||
} else {
|
||||
await updateActiveSync(openedConnection);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
...uploadPromises,
|
||||
...crawlPromises,
|
||||
|
@ -12,9 +12,22 @@
|
||||
height: 16px;
|
||||
border: 1px solid var(--border-2);
|
||||
border-radius: Radius.$small;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&.filled {
|
||||
background-color: var(--primary-0);
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background-color: var(--background-3);
|
||||
opacity: 0.4;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--background-3);
|
||||
}
|
||||
}
|
||||
|
@ -2,16 +2,23 @@ import { useEffect, useState } from "react";
|
||||
|
||||
import styles from "./Checkbox.module.scss";
|
||||
|
||||
import { Icon } from "../Icon/Icon";
|
||||
import Tooltip from "../Tooltip/Tooltip";
|
||||
|
||||
interface CheckboxProps {
|
||||
label: string;
|
||||
label?: string;
|
||||
checked: boolean;
|
||||
setChecked: (value: boolean) => void;
|
||||
disabled?: boolean;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
export const Checkbox = ({
|
||||
label,
|
||||
checked,
|
||||
setChecked,
|
||||
disabled,
|
||||
tooltip,
|
||||
}: CheckboxProps): JSX.Element => {
|
||||
const [currentChecked, setCurrentChecked] = useState<boolean>(checked);
|
||||
|
||||
@ -19,18 +26,31 @@ export const Checkbox = ({
|
||||
setCurrentChecked(checked);
|
||||
}, [checked]);
|
||||
|
||||
return (
|
||||
const checkboxElement = (
|
||||
<div
|
||||
className={styles.checkbox_wrapper}
|
||||
onClick={() => {
|
||||
setChecked(!currentChecked);
|
||||
setCurrentChecked(!currentChecked);
|
||||
className={`${styles.checkbox_wrapper} ${
|
||||
disabled ? styles.disabled : ""
|
||||
}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
if (!disabled) {
|
||||
setChecked(!currentChecked);
|
||||
setCurrentChecked(!currentChecked);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`${styles.checkbox} ${currentChecked ? styles.filled : ""}`}
|
||||
></div>
|
||||
<span>{label}</span>
|
||||
>
|
||||
{currentChecked && <Icon name="check" size="tiny" color="white" />}
|
||||
</div>
|
||||
{label && <span>{label}</span>}
|
||||
</div>
|
||||
);
|
||||
|
||||
return tooltip ? (
|
||||
<Tooltip tooltip={tooltip}>{checkboxElement}</Tooltip>
|
||||
) : (
|
||||
checkboxElement
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,18 @@
|
||||
@use "@/styles/Radius.module.scss";
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
@use "@/styles/Typography.module.scss";
|
||||
|
||||
.connection_icon {
|
||||
border-radius: Radius.$circle;
|
||||
padding: Spacings.$spacing01;
|
||||
color: var(--white-0);
|
||||
min-width: 24px;
|
||||
min-height: 24px;
|
||||
max-height: 24px;
|
||||
max-width: 24px;
|
||||
font-size: Typography.$tiny;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px solid var(--background-0);
|
||||
}
|
22
frontend/lib/components/ui/ConnectionIcon/ConnectionIcon.tsx
Normal file
22
frontend/lib/components/ui/ConnectionIcon/ConnectionIcon.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import styles from "./ConnectionIcon.module.scss";
|
||||
|
||||
interface ConnectionIconProps {
|
||||
letter: string;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export const ConnectionIcon = ({
|
||||
letter,
|
||||
index,
|
||||
}: ConnectionIconProps): JSX.Element => {
|
||||
const colors = ["#FBBC04", "#F28B82", "#8AB4F8", "#81C995", "#C58AF9"];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.connection_icon}
|
||||
style={{ backgroundColor: colors[index % 5] }}
|
||||
>
|
||||
{letter.toUpperCase()}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,5 +1,12 @@
|
||||
@use "@/styles/IconSizes.module.scss";
|
||||
|
||||
.tiny {
|
||||
min-width: IconSizes.$tiny;
|
||||
min-height: IconSizes.$tiny;
|
||||
max-width: IconSizes.$tiny;
|
||||
max-height: IconSizes.$tiny;
|
||||
}
|
||||
|
||||
.small {
|
||||
min-width: IconSizes.$small;
|
||||
min-height: IconSizes.$small;
|
||||
|
@ -23,7 +23,6 @@
|
||||
cursor: auto;
|
||||
box-shadow: BoxShadow.$medium;
|
||||
max-width: 90vw;
|
||||
overflow: scroll;
|
||||
width: 35vw;
|
||||
height: 80vh;
|
||||
|
||||
@ -32,8 +31,8 @@
|
||||
}
|
||||
|
||||
&.big {
|
||||
width: 40vw;
|
||||
height: 90vh;
|
||||
width: 50vw;
|
||||
height: 95vh;
|
||||
}
|
||||
|
||||
&.white {
|
||||
|
@ -14,11 +14,18 @@
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
background-color: var(--background-0);
|
||||
height: fit-content;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.small {
|
||||
padding-inline: Spacings.$spacing03;
|
||||
padding-block: Spacings.$spacing01;
|
||||
font-size: Typography.$tiny;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
border-color: var(--border-2);
|
||||
pointer-events: none;
|
||||
|
@ -17,6 +17,7 @@ export const QuivrButton = ({
|
||||
disabled,
|
||||
hidden,
|
||||
important,
|
||||
small,
|
||||
}: ButtonType): JSX.Element => {
|
||||
const [hovered, setHovered] = useState<boolean>(false);
|
||||
const { isDarkMode } = useUserSettingsContext();
|
||||
@ -36,12 +37,33 @@ export const QuivrButton = ({
|
||||
};
|
||||
|
||||
const getIconColor = () => {
|
||||
let iconColor = color;
|
||||
if (hovered || (important && !disabled)) {
|
||||
return "white";
|
||||
iconColor = "white";
|
||||
} else if (disabled) {
|
||||
return "grey";
|
||||
iconColor = "grey";
|
||||
}
|
||||
|
||||
return iconColor;
|
||||
};
|
||||
|
||||
const renderIcon = () => {
|
||||
if (!isLoading) {
|
||||
return (
|
||||
<Icon
|
||||
name={iconName}
|
||||
size={small ? "small" : "normal"}
|
||||
color={getIconColor()}
|
||||
handleHover={false}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return color;
|
||||
return (
|
||||
<LoaderIcon
|
||||
color={hovered || important ? "white" : disabled ? "grey" : color}
|
||||
size={small ? "small" : "normal"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -54,25 +76,14 @@ export const QuivrButton = ({
|
||||
${hidden ? styles.hidden : ""}
|
||||
${important ? styles.important : ""}
|
||||
${disabled ? styles.disabled : ""}
|
||||
${small ? styles.small : ""}
|
||||
`}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<div className={styles.icon_label}>
|
||||
{!isLoading ? (
|
||||
<Icon
|
||||
name={iconName}
|
||||
size="normal"
|
||||
color={getIconColor()}
|
||||
handleHover={false}
|
||||
/>
|
||||
) : (
|
||||
<LoaderIcon
|
||||
color={hovered || important ? "white" : disabled ? "grey" : color}
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
{renderIcon()}
|
||||
<span className={styles.label}>{label}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -0,0 +1,38 @@
|
||||
@use "@/styles/Radius.module.scss";
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
@use "@/styles/Typography.module.scss";
|
||||
|
||||
.switch_wrapper {
|
||||
display: flex;
|
||||
gap: Spacings.$spacing03;
|
||||
align-items: center;
|
||||
font-size: Typography.$small;
|
||||
font-weight: 500;
|
||||
|
||||
.slider {
|
||||
border-radius: Radius.$big;
|
||||
height: 18px;
|
||||
width: 42px;
|
||||
background-color: var(--primary-2);
|
||||
cursor: pointer;
|
||||
|
||||
&.checked {
|
||||
background-color: var(--primary-1);
|
||||
|
||||
.slider_bubble {
|
||||
margin-left: 26px;
|
||||
background-color: var(--primary-0);
|
||||
}
|
||||
}
|
||||
|
||||
.slider_bubble {
|
||||
margin-left: Spacings.$spacing01;
|
||||
margin-top: Spacings.$spacing01;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
border-radius: Radius.$circle;
|
||||
background-color: var(--primary-1);
|
||||
transition: margin-left 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
31
frontend/lib/components/ui/SwitchButton/SwitchButton.tsx
Normal file
31
frontend/lib/components/ui/SwitchButton/SwitchButton.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import styles from "./SwitchButton.module.scss";
|
||||
|
||||
interface SwitchButtonProps {
|
||||
label: string;
|
||||
checked: boolean;
|
||||
setChecked: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
export const SwitchButton = ({
|
||||
label,
|
||||
checked,
|
||||
setChecked,
|
||||
}: SwitchButtonProps): JSX.Element => {
|
||||
const handleToggle = () => {
|
||||
setChecked(!checked);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.switch_wrapper}>
|
||||
<span>{label}</span>
|
||||
<div
|
||||
className={`${styles.slider} ${checked ? styles.checked : ""}`}
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<div className={styles.slider_bubble}></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SwitchButton;
|
@ -1,6 +1,7 @@
|
||||
@use "@/styles/Radius.module.scss";
|
||||
@use "@/styles/ScreenSizes.module.scss";
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
@use "@/styles/Typography.module.scss";
|
||||
|
||||
.tabs_container {
|
||||
display: flex;
|
||||
@ -37,5 +38,23 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.label_wrapper {
|
||||
display: flex;
|
||||
position: relative;
|
||||
gap: Spacings.$spacing02;
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,12 @@ export const Tabs = ({ tabList }: TabsProps): JSX.Element => {
|
||||
: "black"
|
||||
}
|
||||
/>
|
||||
<span className={styles.label}>{tab.label}</span>
|
||||
<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>
|
||||
|
@ -24,7 +24,7 @@ export const TextAreaInput = ({
|
||||
<textarea
|
||||
className={styles.text_area_input}
|
||||
value={inputValue}
|
||||
rows={5}
|
||||
rows={4}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
placeholder={label}
|
||||
onKeyDown={(e) => {
|
||||
|
@ -1,4 +1,5 @@
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
@use "@/styles/Typography.module.scss";
|
||||
|
||||
.text_button_wrapper {
|
||||
display: flex;
|
||||
@ -12,6 +13,11 @@
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.small {
|
||||
gap: Spacings.$spacing02;
|
||||
font-size: Typography.$tiny;
|
||||
}
|
||||
|
||||
.black {
|
||||
color: var(--text-3);
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { iconList } from "@/lib/helpers/iconList";
|
||||
import { Color } from "@/lib/types/Colors";
|
||||
|
||||
@ -9,20 +11,29 @@ interface TextButtonProps {
|
||||
iconName?: keyof typeof iconList;
|
||||
label: string;
|
||||
color: Color;
|
||||
onClick?: () => void;
|
||||
onClick?: () => void | Promise<void>;
|
||||
disabled?: boolean;
|
||||
small?: boolean;
|
||||
}
|
||||
|
||||
export const TextButton = (props: TextButtonProps): JSX.Element => {
|
||||
const [hovered, setHovered] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.text_button_wrapper} ${
|
||||
props.disabled ? styles.disabled : ""
|
||||
}`}
|
||||
} ${props.small ? styles.small : ""}`}
|
||||
onClick={props.onClick}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
{!!props.iconName && (
|
||||
<Icon name={props.iconName} size="normal" color={props.color} />
|
||||
<Icon
|
||||
name={props.iconName}
|
||||
size={props.small ? "small" : "normal"}
|
||||
color={hovered ? "primary" : props.color}
|
||||
/>
|
||||
)}
|
||||
<span className={styles[props.color]}>{props.label}</span>
|
||||
</div>
|
||||
|
@ -24,9 +24,18 @@ export const useKnowledgeToFeedContext = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const removeAllKnowledgeToFeed = () => {
|
||||
context?.setKnowledgeToFeed([]);
|
||||
};
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error("useKnowledge must be used inside KnowledgeToFeedProvider");
|
||||
}
|
||||
|
||||
return { ...context, addKnowledgeToFeed, removeKnowledgeToFeed };
|
||||
return {
|
||||
...context,
|
||||
addKnowledgeToFeed,
|
||||
removeKnowledgeToFeed,
|
||||
removeAllKnowledgeToFeed,
|
||||
};
|
||||
};
|
||||
|
182
frontend/lib/helpers/handleConnectionButtons.ts
Normal file
182
frontend/lib/helpers/handleConnectionButtons.ts
Normal file
@ -0,0 +1,182 @@
|
||||
import { SetStateAction } from "react";
|
||||
|
||||
import { OpenedConnection } from "../api/sync/types";
|
||||
|
||||
const isRemoveAll = (
|
||||
matchingOpenedConnection: OpenedConnection,
|
||||
currentConnection: OpenedConnection | undefined
|
||||
): boolean => {
|
||||
return !!(
|
||||
currentConnection?.submitted &&
|
||||
matchingOpenedConnection.selectedFiles.files.length === 0 &&
|
||||
!currentConnection.cleaned
|
||||
);
|
||||
};
|
||||
|
||||
const arraysAreEqual = (arr1: string[], arr2: string[]): boolean => {
|
||||
if (arr1.length !== arr2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < arr1.length; i++) {
|
||||
if (arr1[i] !== arr2[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const handleGetButtonProps = (
|
||||
currentConnection: OpenedConnection | undefined,
|
||||
openedConnections: OpenedConnection[],
|
||||
setOpenedConnections: React.Dispatch<
|
||||
React.SetStateAction<OpenedConnection[]>
|
||||
>,
|
||||
currentSyncId: number | undefined,
|
||||
setCurrentSyncId: React.Dispatch<React.SetStateAction<number | undefined>>
|
||||
): {
|
||||
label: string;
|
||||
type: "dangerous" | "primary";
|
||||
disabled: boolean;
|
||||
callback: () => void;
|
||||
} => {
|
||||
const matchingOpenedConnection =
|
||||
currentConnection &&
|
||||
openedConnections.find(
|
||||
(conn) => conn.user_sync_id === currentConnection.user_sync_id
|
||||
);
|
||||
|
||||
if (matchingOpenedConnection) {
|
||||
if (isRemoveAll(matchingOpenedConnection, currentConnection)) {
|
||||
return {
|
||||
label: "Remove All",
|
||||
type: "dangerous",
|
||||
disabled: false,
|
||||
callback: () =>
|
||||
removeConnection(
|
||||
setOpenedConnections,
|
||||
currentSyncId,
|
||||
setCurrentSyncId
|
||||
),
|
||||
};
|
||||
} else if (currentConnection.submitted) {
|
||||
const matchingSelectedFileIds =
|
||||
matchingOpenedConnection.selectedFiles.files
|
||||
.map((file) => file.id)
|
||||
.sort();
|
||||
|
||||
const currentSelectedFileIds = currentConnection.selectedFiles.files
|
||||
.map((file) => file.id)
|
||||
.sort();
|
||||
|
||||
const isDisabled = arraysAreEqual(
|
||||
matchingSelectedFileIds,
|
||||
currentSelectedFileIds
|
||||
);
|
||||
|
||||
return {
|
||||
label: "Update added files",
|
||||
type: "primary",
|
||||
disabled:
|
||||
!matchingOpenedConnection.selectedFiles.files.length || isDisabled,
|
||||
callback: () =>
|
||||
addConnection(setOpenedConnections, currentSyncId, setCurrentSyncId),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
label: "Add specific files",
|
||||
type: "primary",
|
||||
disabled: !matchingOpenedConnection?.selectedFiles.files.length,
|
||||
callback: () =>
|
||||
addConnection(setOpenedConnections, currentSyncId, setCurrentSyncId),
|
||||
};
|
||||
};
|
||||
|
||||
const addConnection = (
|
||||
setOpenedConnections: React.Dispatch<
|
||||
React.SetStateAction<OpenedConnection[]>
|
||||
>,
|
||||
currentSyncId: number | undefined,
|
||||
setCurrentSyncId: React.Dispatch<React.SetStateAction<number | undefined>>
|
||||
): void => {
|
||||
setOpenedConnections((prevConnections) => {
|
||||
const connectionIndex = prevConnections.findIndex(
|
||||
(connection) => connection.user_sync_id === currentSyncId
|
||||
);
|
||||
|
||||
if (connectionIndex !== -1) {
|
||||
const newConnections = [...prevConnections];
|
||||
newConnections[connectionIndex] = {
|
||||
...newConnections[connectionIndex],
|
||||
submitted: true,
|
||||
cleaned: false,
|
||||
};
|
||||
|
||||
return newConnections;
|
||||
}
|
||||
|
||||
return prevConnections;
|
||||
});
|
||||
|
||||
setCurrentSyncId(undefined);
|
||||
};
|
||||
|
||||
const removeConnection = (
|
||||
setOpenedConnections: React.Dispatch<
|
||||
React.SetStateAction<OpenedConnection[]>
|
||||
>,
|
||||
currentSyncId: number | undefined,
|
||||
setCurrentSyncId: React.Dispatch<React.SetStateAction<number | undefined>>
|
||||
): void => {
|
||||
setOpenedConnections((prevConnections) =>
|
||||
prevConnections
|
||||
.filter((connection) => {
|
||||
return (
|
||||
connection.user_sync_id === currentSyncId || !!connection.last_synced
|
||||
);
|
||||
})
|
||||
.map((connection) => {
|
||||
if (
|
||||
connection.user_sync_id === currentSyncId &&
|
||||
!!connection.last_synced
|
||||
) {
|
||||
return { ...connection, cleaned: true };
|
||||
} else {
|
||||
return connection;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
setCurrentSyncId(undefined);
|
||||
};
|
||||
|
||||
export const createHandleGetButtonProps =
|
||||
(
|
||||
currentConnection: OpenedConnection | undefined,
|
||||
openedConnections: OpenedConnection[],
|
||||
setOpenedConnections: {
|
||||
(value: SetStateAction<OpenedConnection[]>): void;
|
||||
(value: SetStateAction<OpenedConnection[]>): void;
|
||||
},
|
||||
currentSyncId: number | undefined,
|
||||
setCurrentSyncId: {
|
||||
(value: SetStateAction<number | undefined>): void;
|
||||
(value: SetStateAction<number | undefined>): void;
|
||||
}
|
||||
) =>
|
||||
(): {
|
||||
label: string;
|
||||
type: "primary" | "dangerous";
|
||||
disabled: boolean;
|
||||
callback: () => void;
|
||||
} =>
|
||||
handleGetButtonProps(
|
||||
currentConnection,
|
||||
openedConnections,
|
||||
setOpenedConnections,
|
||||
currentSyncId,
|
||||
setCurrentSyncId
|
||||
);
|
@ -13,10 +13,10 @@ import {
|
||||
FaCheckCircle,
|
||||
FaDiscord,
|
||||
FaFileAlt,
|
||||
FaFolder,
|
||||
FaGithub,
|
||||
FaKey,
|
||||
FaLinkedin,
|
||||
FaMoon,
|
||||
FaQuestionCircle,
|
||||
FaRegFileAlt,
|
||||
FaRegKeyboard,
|
||||
@ -24,7 +24,6 @@ import {
|
||||
FaRegThumbsDown,
|
||||
FaRegThumbsUp,
|
||||
FaRegUserCircle,
|
||||
FaSun,
|
||||
FaTwitter,
|
||||
FaUnlock,
|
||||
} from "react-icons/fa";
|
||||
@ -38,13 +37,14 @@ import {
|
||||
IoIosRadio,
|
||||
IoMdClose,
|
||||
IoMdLogOut,
|
||||
IoMdSettings,
|
||||
IoMdSync,
|
||||
} from "react-icons/io";
|
||||
import {
|
||||
IoArrowUpCircleOutline,
|
||||
IoCloudDownloadOutline,
|
||||
IoFootsteps,
|
||||
IoHomeOutline,
|
||||
IoSettingsSharp,
|
||||
IoShareSocial,
|
||||
IoWarningOutline,
|
||||
} from "react-icons/io5";
|
||||
@ -63,6 +63,7 @@ import {
|
||||
} from "react-icons/lu";
|
||||
import {
|
||||
MdAlternateEmail,
|
||||
MdDarkMode,
|
||||
MdDashboardCustomize,
|
||||
MdDeleteOutline,
|
||||
MdDynamicFeed,
|
||||
@ -70,6 +71,7 @@ import {
|
||||
MdLink,
|
||||
MdMarkEmailRead,
|
||||
MdMarkEmailUnread,
|
||||
MdOutlineLightMode,
|
||||
MdOutlineModeEditOutline,
|
||||
MdUnfoldLess,
|
||||
MdUnfoldMore,
|
||||
@ -110,6 +112,7 @@ export const iconList: { [name: string]: IconType } = {
|
||||
fileSelected: FaFileAlt,
|
||||
flag: CiFlag1,
|
||||
fold: MdUnfoldLess,
|
||||
folder: FaFolder,
|
||||
followUp: IoArrowUpCircleOutline,
|
||||
github: FaGithub,
|
||||
goal: LuGoal,
|
||||
@ -124,7 +127,7 @@ export const iconList: { [name: string]: IconType } = {
|
||||
linkedin: FaLinkedin,
|
||||
loader: AiOutlineLoading3Quarters,
|
||||
logout: IoMdLogOut,
|
||||
moon: FaMoon,
|
||||
moon: MdDarkMode,
|
||||
notifications: IoIosNotifications,
|
||||
office: HiBuildingOffice,
|
||||
options: SlOptions,
|
||||
@ -136,12 +139,13 @@ export const iconList: { [name: string]: IconType } = {
|
||||
read: MdMarkEmailRead,
|
||||
robot: LiaRobotSolid,
|
||||
search: LuSearch,
|
||||
settings: IoSettingsSharp,
|
||||
settings: IoMdSettings,
|
||||
share: IoShareSocial,
|
||||
software: CgSoftwareDownload,
|
||||
star: FaRegStar,
|
||||
step: IoFootsteps,
|
||||
sun: FaSun,
|
||||
sun: MdOutlineLightMode,
|
||||
sync: IoMdSync,
|
||||
thumbsDown: FaRegThumbsDown,
|
||||
thumbsUp: FaRegThumbsUp,
|
||||
twitter: FaTwitter,
|
||||
|
@ -1 +1 @@
|
||||
export type IconSize = "small" | "normal" | "large" | "big";
|
||||
export type IconSize = "tiny" | "small" | "normal" | "large" | "big";
|
||||
|
@ -11,4 +11,5 @@ export interface ButtonType {
|
||||
disabled?: boolean;
|
||||
hidden?: boolean;
|
||||
important?: boolean;
|
||||
small?: boolean;
|
||||
}
|
||||
|
@ -6,4 +6,5 @@ export interface Tab {
|
||||
disabled?: boolean;
|
||||
iconName: keyof typeof iconList;
|
||||
onClick: () => void;
|
||||
badge?: number;
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
$tiny: 10px;
|
||||
$small: 14px;
|
||||
$normal: 18px;
|
||||
$large: 24px;
|
||||
|
@ -10,12 +10,12 @@
|
||||
|
||||
@mixin H2 {
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
font-size: $large;
|
||||
}
|
||||
|
||||
@mixin H3 {
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
font-size: $medium;
|
||||
}
|
||||
|
||||
@mixin EllipsisOverflow {
|
||||
|
Loading…
Reference in New Issue
Block a user