mirror of
https://github.com/StanGirard/quivr.git
synced 2024-08-17 17:10:22 +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.sync.service.sync_service import SyncService, SyncUserService
|
||||||
from modules.user.entity.user_identity import UserIdentity
|
from modules.user.entity.user_identity import UserIdentity
|
||||||
from msal import PublicClientApplication
|
from msal import PublicClientApplication
|
||||||
|
from .successfull_connection import successfullConnectionPage
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
|
||||||
|
|
||||||
# Initialize logger
|
# Initialize logger
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
@ -31,7 +34,7 @@ SCOPE = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@azure_sync_router.get(
|
@azure_sync_router.post(
|
||||||
"/sync/azure/authorize",
|
"/sync/azure/authorize",
|
||||||
dependencies=[Depends(AuthBearer())],
|
dependencies=[Depends(AuthBearer())],
|
||||||
tags=["Sync"],
|
tags=["Sync"],
|
||||||
@ -125,4 +128,4 @@ def oauth2callback_azure(request: Request):
|
|||||||
|
|
||||||
sync_user_service.update_sync_user(current_user, state_dict, sync_user_input)
|
sync_user_service.update_sync_user(current_user, state_dict, sync_user_input)
|
||||||
logger.info(f"Azure sync created successfully for user: {current_user}")
|
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.dto.inputs import SyncsUserInput, SyncUserUpdateInput
|
||||||
from modules.sync.service.sync_service import SyncService, SyncUserService
|
from modules.sync.service.sync_service import SyncService, SyncUserService
|
||||||
from modules.user.entity.user_identity import UserIdentity
|
from modules.user.entity.user_identity import UserIdentity
|
||||||
|
from .successfull_connection import successfullConnectionPage
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
|
||||||
|
|
||||||
# Set environment variable for OAuthlib
|
# Set environment variable for OAuthlib
|
||||||
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
|
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",
|
"/sync/google/authorize",
|
||||||
dependencies=[Depends(AuthBearer())],
|
dependencies=[Depends(AuthBearer())],
|
||||||
tags=["Sync"],
|
tags=["Sync"],
|
||||||
@ -138,4 +141,4 @@ def oauth2callback_google(request: Request):
|
|||||||
)
|
)
|
||||||
sync_user_service.update_sync_user(current_user, state_dict, sync_user_input)
|
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}")
|
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],
|
complexity: ["error", 10],
|
||||||
"max-lines": ["error", 300],
|
"max-lines": ["error", 300],
|
||||||
"max-depth": ["error", 3],
|
"max-depth": ["error", 3],
|
||||||
"max-params": ["error", 4],
|
"max-params": ["error", 5],
|
||||||
eqeqeq: ["error", "smart"],
|
eqeqeq: ["error", "smart"],
|
||||||
"import/no-extraneous-dependencies": [
|
"import/no-extraneous-dependencies": [
|
||||||
"error",
|
"error",
|
||||||
|
@ -24,7 +24,9 @@ import { UserSettingsProvider } from "@/lib/context/UserSettingsProvider/User-se
|
|||||||
import { IntercomProvider } from "@/lib/helpers/intercom/IntercomProvider";
|
import { IntercomProvider } from "@/lib/helpers/intercom/IntercomProvider";
|
||||||
import { UpdateMetadata } from "@/lib/helpers/updateMetadata";
|
import { UpdateMetadata } from "@/lib/helpers/updateMetadata";
|
||||||
import { usePageTracking } from "@/services/analytics/june/usePageTracking";
|
import { usePageTracking } from "@/services/analytics/june/usePageTracking";
|
||||||
|
|
||||||
import "../lib/config/LocaleConfig/i18n";
|
import "../lib/config/LocaleConfig/i18n";
|
||||||
|
import { FromConnectionsProvider } from "./chat/[chatId]/components/ActionsBar/components/KnowledgeToFeed/components/FromConnections/FromConnectionsProvider/FromConnection-provider";
|
||||||
|
|
||||||
if (
|
if (
|
||||||
process.env.NEXT_PUBLIC_POSTHOG_KEY != null &&
|
process.env.NEXT_PUBLIC_POSTHOG_KEY != null &&
|
||||||
@ -90,11 +92,13 @@ const AppWithQueryClient = ({ children }: PropsWithChildren): JSX.Element => {
|
|||||||
<BrainCreationProvider>
|
<BrainCreationProvider>
|
||||||
<MenuProvider>
|
<MenuProvider>
|
||||||
<OnboardingProvider>
|
<OnboardingProvider>
|
||||||
<ChatsProvider>
|
<FromConnectionsProvider>
|
||||||
<ChatProvider>
|
<ChatsProvider>
|
||||||
<App>{children}</App>
|
<ChatProvider>
|
||||||
</ChatProvider>
|
<App>{children}</App>
|
||||||
</ChatsProvider>
|
</ChatProvider>
|
||||||
|
</ChatsProvider>
|
||||||
|
</FromConnectionsProvider>
|
||||||
</OnboardingProvider>
|
</OnboardingProvider>
|
||||||
</MenuProvider>
|
</MenuProvider>
|
||||||
</BrainCreationProvider>
|
</BrainCreationProvider>
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
gap: Spacings.$spacing05;
|
gap: Spacings.$spacing05;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
.single_selector_wrapper {
|
.single_selector_wrapper {
|
||||||
width: 30%;
|
width: 30%;
|
||||||
@ -21,44 +22,8 @@
|
|||||||
|
|
||||||
.tabs_content_wrapper {
|
.tabs_content_wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 200px;
|
height: 80%;
|
||||||
}
|
|
||||||
|
|
||||||
.uploaded_knowledges_title {
|
|
||||||
color: var(--text-2);
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uploaded_knowledges {
|
|
||||||
padding: Spacings.$spacing03;
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
flex-direction: column;
|
padding: Spacings.$spacing01;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 { SingleSelector } from "@/lib/components/ui/SingleSelector/SingleSelector";
|
||||||
import { Tabs } from "@/lib/components/ui/Tabs/Tabs";
|
import { Tabs } from "@/lib/components/ui/Tabs/Tabs";
|
||||||
import { requiredRolesForUpload } from "@/lib/config/upload";
|
import { requiredRolesForUpload } from "@/lib/config/upload";
|
||||||
@ -9,6 +9,8 @@ import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider
|
|||||||
import { Tab } from "@/lib/types/Tab";
|
import { Tab } from "@/lib/types/Tab";
|
||||||
|
|
||||||
import styles from "./KnowledgeToFeed.module.scss";
|
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 { FromDocuments } from "./components/FromDocuments/FromDocuments";
|
||||||
import { FromWebsites } from "./components/FromWebsites/FromWebsites";
|
import { FromWebsites } from "./components/FromWebsites/FromWebsites";
|
||||||
import { formatMinimalBrainsToSelectComponentInput } from "./utils/formatMinimalBrainsToSelectComponentInput";
|
import { formatMinimalBrainsToSelectComponentInput } from "./utils/formatMinimalBrainsToSelectComponentInput";
|
||||||
@ -18,10 +20,13 @@ export const KnowledgeToFeed = ({
|
|||||||
}: {
|
}: {
|
||||||
hideBrainSelector?: boolean;
|
hideBrainSelector?: boolean;
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const { allBrains, setCurrentBrainId, currentBrain } = useBrainContext();
|
const { allBrains, setCurrentBrainId, currentBrainId, currentBrain } =
|
||||||
const [selectedTab, setSelectedTab] = useState("From documents");
|
useBrainContext();
|
||||||
const { knowledgeToFeed, removeKnowledgeToFeed } =
|
const [selectedTab, setSelectedTab] = useState("Documents");
|
||||||
useKnowledgeToFeedContext();
|
const { knowledgeToFeed } = useKnowledgeToFeedContext();
|
||||||
|
const { openedConnections, setOpenedConnections, setCurrentSyncId } =
|
||||||
|
useFromConnectionsContext();
|
||||||
|
const { getActiveSyncsForBrain } = useSync();
|
||||||
|
|
||||||
const brainsWithUploadRights = formatMinimalBrainsToSelectComponentInput(
|
const brainsWithUploadRights = formatMinimalBrainsToSelectComponentInput(
|
||||||
useMemo(
|
useMemo(
|
||||||
@ -36,19 +41,69 @@ export const KnowledgeToFeed = ({
|
|||||||
|
|
||||||
const knowledgesTabs: Tab[] = [
|
const knowledgesTabs: Tab[] = [
|
||||||
{
|
{
|
||||||
label: "From documents",
|
label: "Documents",
|
||||||
isSelected: selectedTab === "From documents",
|
isSelected: selectedTab === "Documents",
|
||||||
onClick: () => setSelectedTab("From documents"),
|
onClick: () => setSelectedTab("Documents"),
|
||||||
iconName: "file",
|
iconName: "file",
|
||||||
|
badge: knowledgeToFeed.filter(
|
||||||
|
(knowledge) => knowledge.source === "upload"
|
||||||
|
).length,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "From websites",
|
label: "Websites",
|
||||||
isSelected: selectedTab === "From websites",
|
isSelected: selectedTab === "Websites",
|
||||||
onClick: () => setSelectedTab("From websites"),
|
onClick: () => setSelectedTab("Websites"),
|
||||||
iconName: "website",
|
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 (
|
return (
|
||||||
<div className={styles.knowledge_to_feed_wrapper}>
|
<div className={styles.knowledge_to_feed_wrapper}>
|
||||||
{!hideBrainSelector && (
|
{!hideBrainSelector && (
|
||||||
@ -68,39 +123,9 @@ export const KnowledgeToFeed = ({
|
|||||||
)}
|
)}
|
||||||
<Tabs tabList={knowledgesTabs} />
|
<Tabs tabList={knowledgesTabs} />
|
||||||
<div className={styles.tabs_content_wrapper}>
|
<div className={styles.tabs_content_wrapper}>
|
||||||
{selectedTab === "From documents" && <FromDocuments />}
|
{selectedTab === "Documents" && <FromDocuments />}
|
||||||
{selectedTab === "From websites" && <FromWebsites />}
|
{selectedTab === "Websites" && <FromWebsites />}
|
||||||
</div>
|
{selectedTab === "Connections" && <FromConnections />}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</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 {
|
.from_document_wrapper {
|
||||||
width: 100%;
|
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: 1px dashed var(--border-0);
|
||||||
border-radius: Radius.$big;
|
border-radius: Radius.$big;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
height: 100%;
|
||||||
|
overflow: scroll;
|
||||||
|
|
||||||
&.dragging {
|
&.dragging {
|
||||||
border: 3px dashed var(--accent);
|
border: 3px dashed var(--accent);
|
||||||
background-color: var(--background-3);
|
background-color: var(--background-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.box_content {
|
||||||
padding: Spacings.$spacing05;
|
padding: Spacings.$spacing05;
|
||||||
display: flex;
|
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) {
|
.input {
|
||||||
flex-direction: column;
|
display: flex;
|
||||||
}
|
gap: Spacings.$spacing02;
|
||||||
|
padding: Spacings.$spacing05;
|
||||||
|
|
||||||
.clickable {
|
@media (max-width: ScreenSizes.$small) {
|
||||||
font-weight: bold;
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,13 +27,15 @@ export const FromDocuments = (): JSX.Element => {
|
|||||||
onMouseLeave={() => setDragging(false)}
|
onMouseLeave={() => setDragging(false)}
|
||||||
onClick={open}
|
onClick={open}
|
||||||
>
|
>
|
||||||
<Icon name="upload" size="big" color={dragging ? "accent" : "black"} />
|
<div className={styles.box_content}>
|
||||||
<div className={styles.input}>
|
<Icon name="upload" size="big" color={dragging ? "accent" : "black"} />
|
||||||
<div className={styles.clickable}>
|
<div className={styles.input}>
|
||||||
<span>Choose files</span>
|
<div className={styles.clickable}>
|
||||||
<input {...getInputProps()} />
|
<span>Choose files</span>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
</div>
|
||||||
|
<span>or drag it here</span>
|
||||||
</div>
|
</div>
|
||||||
<span>or drag it here</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -17,6 +17,7 @@ import { useToast } from "@/lib/hooks";
|
|||||||
import { useOnboarding } from "@/lib/hooks/useOnboarding";
|
import { useOnboarding } from "@/lib/hooks/useOnboarding";
|
||||||
|
|
||||||
import { FeedItemCrawlType, FeedItemUploadType } from "../../../types";
|
import { FeedItemCrawlType, FeedItemUploadType } from "../../../types";
|
||||||
|
import { useFromConnectionsContext } from "../components/FromConnections/FromConnectionsProvider/hooks/useFromConnectionContext";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
export const useFeedBrainInChat = ({
|
export const useFeedBrainInChat = ({
|
||||||
@ -42,6 +43,7 @@ export const useFeedBrainInChat = ({
|
|||||||
const fetchedNotifications = await getChatNotifications(currentChatId);
|
const fetchedNotifications = await getChatNotifications(currentChatId);
|
||||||
setNotifications(fetchedNotifications);
|
setNotifications(fetchedNotifications);
|
||||||
};
|
};
|
||||||
|
const { openedConnections } = useFromConnectionsContext();
|
||||||
const { crawlWebsiteHandler, uploadFileHandler } = useKnowledgeToFeedInput();
|
const { crawlWebsiteHandler, uploadFileHandler } = useKnowledgeToFeedInput();
|
||||||
const files: File[] = (
|
const files: File[] = (
|
||||||
knowledgeToFeed.filter((c) => c.source === "upload") as FeedItemUploadType[]
|
knowledgeToFeed.filter((c) => c.source === "upload") as FeedItemUploadType[]
|
||||||
@ -58,7 +60,7 @@ export const useFeedBrainInChat = ({
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (knowledgeToFeed.length === 0) {
|
if (knowledgeToFeed.length === 0 && !openedConnections.length) {
|
||||||
publish({
|
publish({
|
||||||
variant: "danger",
|
variant: "danger",
|
||||||
text: t("addFiles"),
|
text: t("addFiles"),
|
||||||
|
@ -85,8 +85,8 @@ div:focus {
|
|||||||
|
|
||||||
body.dark_mode {
|
body.dark_mode {
|
||||||
/* Backgrounds */
|
/* Backgrounds */
|
||||||
--background-0: var(--black-0);
|
--background-0: var(--black-1);
|
||||||
--background-1: var(--black-1);
|
--background-1: var(--black-0);
|
||||||
--background-2: var(--black-2);
|
--background-2: var(--black-2);
|
||||||
--background-3: var(--black-3);
|
--background-3: var(--black-3);
|
||||||
--background-4: var(--black-4);
|
--background-4: var(--black-4);
|
||||||
|
@ -1,20 +1,15 @@
|
|||||||
@use "@/styles/Spacings.module.scss";
|
@use "@/styles/Spacings.module.scss";
|
||||||
@use "@/styles/Typography.module.scss";
|
@use "@/styles/Typography.module.scss";
|
||||||
|
|
||||||
.cards_wrapper {
|
.connections_wrapper {
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: Spacings.$spacing05;
|
gap: Spacings.$spacing05;
|
||||||
|
padding: Spacings.$spacing06;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
@include Typography.H2;
|
@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/Radius.module.scss";
|
||||||
@use "@/styles/Spacings.module.scss";
|
@use "@/styles/Spacings.module.scss";
|
||||||
|
@use "@/styles/Typography.module.scss";
|
||||||
|
|
||||||
.settings_wrapper {
|
.settings_wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: Spacings.$spacing07;
|
gap: Spacings.$spacing07;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
padding: Spacings.$spacing06;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
@include Typography.H2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,9 @@ type InfoDisplayerProps = {
|
|||||||
export const Settings = ({ email }: InfoDisplayerProps): JSX.Element => {
|
export const Settings = ({ email }: InfoDisplayerProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.settings_wrapper}>
|
<div className={styles.settings_wrapper}>
|
||||||
|
<span className={styles.title}>
|
||||||
|
General settings and main information
|
||||||
|
</span>
|
||||||
<InfoDisplayer label="Email" iconName="email">
|
<InfoDisplayer label="Email" iconName="email">
|
||||||
<span>{email}</span>
|
<span>{email}</span>
|
||||||
</InfoDisplayer>
|
</InfoDisplayer>
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
@use "@/styles/Radius.module.scss";
|
||||||
|
@use "@/styles/ScreenSizes.module.scss";
|
||||||
@use "@/styles/Spacings.module.scss";
|
@use "@/styles/Spacings.module.scss";
|
||||||
|
|
||||||
.user_page_container {
|
.user_page_container {
|
||||||
@ -5,7 +7,18 @@
|
|||||||
padding-block: Spacings.$spacing07;
|
padding-block: Spacings.$spacing07;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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 {
|
.modal_wrapper {
|
||||||
|
@ -7,11 +7,14 @@ import { useUserApi } from "@/lib/api/user/useUserApi";
|
|||||||
import PageHeader from "@/lib/components/PageHeader/PageHeader";
|
import PageHeader from "@/lib/components/PageHeader/PageHeader";
|
||||||
import { Modal } from "@/lib/components/ui/Modal/Modal";
|
import { Modal } from "@/lib/components/ui/Modal/Modal";
|
||||||
import QuivrButton from "@/lib/components/ui/QuivrButton/QuivrButton";
|
import QuivrButton from "@/lib/components/ui/QuivrButton/QuivrButton";
|
||||||
|
import { Tabs } from "@/lib/components/ui/Tabs/Tabs";
|
||||||
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
||||||
import { useUserData } from "@/lib/hooks/useUserData";
|
import { useUserData } from "@/lib/hooks/useUserData";
|
||||||
import { redirectToLogin } from "@/lib/router/redirectToLogin";
|
import { redirectToLogin } from "@/lib/router/redirectToLogin";
|
||||||
import { ButtonType } from "@/lib/types/QuivrButton";
|
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 { Settings } from "./components/Settings/Settings";
|
||||||
import styles from "./page.module.scss";
|
import styles from "./page.module.scss";
|
||||||
|
|
||||||
@ -30,6 +33,7 @@ const UserPage = (): JSX.Element => {
|
|||||||
isLogoutModalOpened,
|
isLogoutModalOpened,
|
||||||
setIsLogoutModalOpened,
|
setIsLogoutModalOpened,
|
||||||
} = useLogoutModal();
|
} = useLogoutModal();
|
||||||
|
const [selectedTab, setSelectedTab] = useState("Connections");
|
||||||
|
|
||||||
const buttons: ButtonType[] = [
|
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) {
|
if (!session || !userData) {
|
||||||
redirectToLogin();
|
redirectToLogin();
|
||||||
}
|
}
|
||||||
@ -60,8 +79,11 @@ const UserPage = (): JSX.Element => {
|
|||||||
<PageHeader iconName="user" label="Profile" buttons={buttons} />
|
<PageHeader iconName="user" label="Profile" buttons={buttons} />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.user_page_container}>
|
<div className={styles.user_page_container}>
|
||||||
|
<Tabs tabList={studioTabs} />
|
||||||
|
<div className={styles.user_page_menu}></div>
|
||||||
<div className={styles.content_wrapper}>
|
<div className={styles.content_wrapper}>
|
||||||
<Settings email={userData.email} />
|
{selectedTab === "General" && <Settings email={userData.email} />}
|
||||||
|
{selectedTab === "Connections" && <Connections />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Modal
|
<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;
|
padding-block: Spacings.$spacing05;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
max-height: 90%;
|
||||||
|
min-height: 90%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
gap: Spacings.$spacing08;
|
gap: Spacings.$spacing05;
|
||||||
|
|
||||||
.stepper_container {
|
.stepper_container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -16,6 +17,6 @@
|
|||||||
|
|
||||||
.content_wrapper {
|
.content_wrapper {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow: scroll;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,15 +2,17 @@ import { useEffect } from "react";
|
|||||||
import { FormProvider, useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { Modal } from "@/lib/components/ui/Modal/Modal";
|
||||||
import { addBrainDefaultValues } from "@/lib/config/defaultBrainConfig";
|
import { addBrainDefaultValues } from "@/lib/config/defaultBrainConfig";
|
||||||
|
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
|
||||||
import { useUserData } from "@/lib/hooks/useUserData";
|
import { useUserData } from "@/lib/hooks/useUserData";
|
||||||
|
|
||||||
import styles from "./AddBrainModal.module.scss";
|
import styles from "./AddBrainModal.module.scss";
|
||||||
import { useBrainCreationContext } from "./brainCreation-provider";
|
import { useBrainCreationContext } from "./brainCreation-provider";
|
||||||
import { BrainMainInfosStep } from "./components/BrainMainInfosStep/BrainMainInfosStep";
|
import { BrainMainInfosStep } from "./components/BrainMainInfosStep/BrainMainInfosStep";
|
||||||
import { BrainTypeSelectionStep } from "./components/BrainTypeSelectionStep/BrainTypeSelectionStep";
|
import { BrainRecapStep } from "./components/BrainRecapStep/BrainRecapStep";
|
||||||
import { CreateBrainStep } from "./components/CreateBrainStep/CreateBrainStep";
|
import { FeedBrainStep } from "./components/FeedBrainStep/FeedBrainStep";
|
||||||
import { Stepper } from "./components/Stepper/Stepper";
|
import { Stepper } from "./components/Stepper/Stepper";
|
||||||
import { useBrainCreationSteps } from "./hooks/useBrainCreationSteps";
|
import { useBrainCreationSteps } from "./hooks/useBrainCreationSteps";
|
||||||
import { CreateBrainProps } from "./types/types";
|
import { CreateBrainProps } from "./types/types";
|
||||||
@ -19,12 +21,14 @@ export const AddBrainModal = (): JSX.Element => {
|
|||||||
const { t } = useTranslation(["translation", "brain", "config"]);
|
const { t } = useTranslation(["translation", "brain", "config"]);
|
||||||
const { userIdentityData } = useUserData();
|
const { userIdentityData } = useUserData();
|
||||||
const { currentStep, steps } = useBrainCreationSteps();
|
const { currentStep, steps } = useBrainCreationSteps();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isBrainCreationModalOpened,
|
isBrainCreationModalOpened,
|
||||||
setIsBrainCreationModalOpened,
|
setIsBrainCreationModalOpened,
|
||||||
setCurrentSelectedBrain,
|
setCurrentSelectedBrain,
|
||||||
} = useBrainCreationContext();
|
} = useBrainCreationContext();
|
||||||
|
const { setCurrentSyncId, setOpenedConnections } =
|
||||||
|
useFromConnectionsContext();
|
||||||
|
const { removeAllKnowledgeToFeed } = useKnowledgeToFeedContext();
|
||||||
|
|
||||||
const defaultValues: CreateBrainProps = {
|
const defaultValues: CreateBrainProps = {
|
||||||
...addBrainDefaultValues,
|
...addBrainDefaultValues,
|
||||||
@ -38,6 +42,10 @@ export const AddBrainModal = (): JSX.Element => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentSelectedBrain(undefined);
|
setCurrentSelectedBrain(undefined);
|
||||||
|
setCurrentSyncId(undefined);
|
||||||
|
setOpenedConnections([]);
|
||||||
|
methods.reset(defaultValues);
|
||||||
|
removeAllKnowledgeToFeed();
|
||||||
}, [isBrainCreationModalOpened]);
|
}, [isBrainCreationModalOpened]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -56,9 +64,9 @@ export const AddBrainModal = (): JSX.Element => {
|
|||||||
<Stepper currentStep={currentStep} steps={steps} />
|
<Stepper currentStep={currentStep} steps={steps} />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.content_wrapper}>
|
<div className={styles.content_wrapper}>
|
||||||
<BrainTypeSelectionStep />
|
|
||||||
<BrainMainInfosStep />
|
<BrainMainInfosStep />
|
||||||
<CreateBrainStep />
|
<FeedBrainStep />
|
||||||
|
<BrainRecapStep />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
@use "@/styles/ScreenSizes.module.scss";
|
||||||
@use "@/styles/Spacings.module.scss";
|
@use "@/styles/Spacings.module.scss";
|
||||||
@use "@/styles/Typography.module.scss";
|
@use "@/styles/Typography.module.scss";
|
||||||
|
|
||||||
@ -7,6 +8,7 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding-inline: Spacings.$spacing08;
|
padding-inline: Spacings.$spacing08;
|
||||||
|
gap: Spacings.$spacing03;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
@include Typography.H2;
|
@include Typography.H2;
|
||||||
@ -16,10 +18,21 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: Spacings.$spacing05;
|
gap: Spacings.$spacing05;
|
||||||
|
overflow: scroll;
|
||||||
|
|
||||||
|
.name_field {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: ScreenSizes.$small) {
|
||||||
|
.name_field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttons_wrapper {
|
.buttons_wrapper {
|
||||||
display: flex;
|
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";
|
import { useBrainCreationSteps } from "../../hooks/useBrainCreationSteps";
|
||||||
|
|
||||||
export const BrainMainInfosStep = (): JSX.Element => {
|
export const BrainMainInfosStep = (): JSX.Element => {
|
||||||
const { currentStepIndex, goToNextStep, goToPreviousStep } =
|
const { currentStepIndex, goToNextStep } = useBrainCreationSteps();
|
||||||
useBrainCreationSteps();
|
|
||||||
|
|
||||||
const { watch } = useFormContext<CreateBrainProps>();
|
const { watch } = useFormContext<CreateBrainProps>();
|
||||||
const name = watch("name");
|
const name = watch("name");
|
||||||
@ -24,11 +23,7 @@ export const BrainMainInfosStep = (): JSX.Element => {
|
|||||||
goToNextStep();
|
goToNextStep();
|
||||||
};
|
};
|
||||||
|
|
||||||
const previous = (): void => {
|
if (currentStepIndex !== 0) {
|
||||||
goToPreviousStep();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (currentStepIndex !== 1) {
|
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,7 +31,7 @@ export const BrainMainInfosStep = (): JSX.Element => {
|
|||||||
<div className={styles.brain_main_infos_wrapper}>
|
<div className={styles.brain_main_infos_wrapper}>
|
||||||
<div className={styles.inputs_wrapper}>
|
<div className={styles.inputs_wrapper}>
|
||||||
<span className={styles.title}>Define brain identity</span>
|
<span className={styles.title}>Define brain identity</span>
|
||||||
<div>
|
<div className={styles.name_field}>
|
||||||
<FieldHeader iconName="brain" label="Name" mandatory={true} />
|
<FieldHeader iconName="brain" label="Name" mandatory={true} />
|
||||||
<Controller
|
<Controller
|
||||||
name="name"
|
name="name"
|
||||||
@ -68,12 +63,6 @@ export const BrainMainInfosStep = (): JSX.Element => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.buttons_wrapper}>
|
<div className={styles.buttons_wrapper}>
|
||||||
<QuivrButton
|
|
||||||
color="primary"
|
|
||||||
label="Previous Step"
|
|
||||||
onClick={() => previous()}
|
|
||||||
iconName="chevronLeft"
|
|
||||||
/>
|
|
||||||
<QuivrButton
|
<QuivrButton
|
||||||
color="primary"
|
color="primary"
|
||||||
label="Next Step"
|
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;
|
padding-inline: Spacings.$spacing08;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
.title {
|
.feed_brain {
|
||||||
@include Typography.H2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings_wrapper {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: Spacings.$spacing05;
|
overflow: scroll;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
@include Typography.H2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.message_info_box_wrapper {
|
.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 { useFormContext } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { PUBLIC_BRAINS_KEY } from "@/lib/api/brain/config";
|
||||||
import { IntegrationSettings } from "@/lib/api/brain/types";
|
import { IntegrationSettings } from "@/lib/api/brain/types";
|
||||||
|
import { useSync } from "@/lib/api/sync/useSync";
|
||||||
import { CreateBrainProps } from "@/lib/components/AddBrainModal/types/types";
|
import { CreateBrainProps } from "@/lib/components/AddBrainModal/types/types";
|
||||||
import { useKnowledgeToFeedInput } from "@/lib/components/KnowledgeToFeedInput/hooks/useKnowledgeToFeedInput.ts";
|
import { useKnowledgeToFeedInput } from "@/lib/components/KnowledgeToFeedInput/hooks/useKnowledgeToFeedInput.ts";
|
||||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||||
@ -31,14 +33,22 @@ export const useBrainCreationApi = () => {
|
|||||||
const [fields, setFields] = useState<
|
const [fields, setFields] = useState<
|
||||||
{ name: string; type: string; value: string }[]
|
{ name: string; type: string; value: string }[]
|
||||||
>([]);
|
>([]);
|
||||||
|
const { syncFiles } = useSync();
|
||||||
|
const { openedConnections } = useFromConnectionsContext();
|
||||||
|
|
||||||
const handleFeedBrain = async (brainId: UUID): Promise<void> => {
|
const handleFeedBrain = async (brainId: UUID): Promise<void> => {
|
||||||
const uploadPromises = files.map((file) =>
|
const uploadPromises = files.map((file) =>
|
||||||
uploadFileHandler(file, brainId)
|
uploadFileHandler(file, brainId)
|
||||||
);
|
);
|
||||||
|
|
||||||
const crawlPromises = urls.map((url) => crawlWebsiteHandler(url, brainId));
|
const crawlPromises = urls.map((url) => crawlWebsiteHandler(url, brainId));
|
||||||
|
|
||||||
await Promise.all([...uploadPromises, ...crawlPromises]);
|
await Promise.all([...uploadPromises, ...crawlPromises]);
|
||||||
|
await Promise.all(
|
||||||
|
openedConnections.map(async (openedConnection) => {
|
||||||
|
await syncFiles(openedConnection, brainId);
|
||||||
|
})
|
||||||
|
);
|
||||||
setKnowledgeToFeed([]);
|
setKnowledgeToFeed([]);
|
||||||
};
|
};
|
||||||
|
|
@ -14,8 +14,8 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.circle {
|
.circle {
|
||||||
width: 2.5rem;
|
width: 1.75rem;
|
||||||
height: 2.5rem;
|
height: 1.75rem;
|
||||||
background-color: var(--primary-0);
|
background-color: var(--primary-0);
|
||||||
border-radius: Radius.$circle;
|
border-radius: Radius.$circle;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -80,7 +80,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
font-size: Typography.$tiny;
|
font-size: Typography.$tiny;
|
||||||
width: 2.5rem;
|
width: 1.75rem;
|
||||||
|
|
||||||
.step_index {
|
.step_index {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@ -95,7 +95,7 @@
|
|||||||
border-radius: Radius.$big;
|
border-radius: Radius.$big;
|
||||||
background-color: var(--primary-1);
|
background-color: var(--primary-1);
|
||||||
margin: 0 8px;
|
margin: 0 8px;
|
||||||
margin-top: Spacings.$spacing05;
|
margin-top: Spacings.$spacing04;
|
||||||
|
|
||||||
&.done {
|
&.done {
|
||||||
background-color: var(--success);
|
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 />}
|
{!isMobile && <Notifications />}
|
||||||
|
<Icon
|
||||||
|
name="settings"
|
||||||
|
color="black"
|
||||||
|
handleHover={true}
|
||||||
|
size="normal"
|
||||||
|
onClick={() => void (window.location.href = "/user")}
|
||||||
|
/>
|
||||||
<Icon
|
<Icon
|
||||||
name={lightModeIconName}
|
name={lightModeIconName}
|
||||||
color="black"
|
color="black"
|
||||||
handleHover={true}
|
handleHover={true}
|
||||||
size="small"
|
size="normal"
|
||||||
onClick={toggleTheme}
|
onClick={toggleTheme}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,8 +10,12 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
.button {
|
.buttons {
|
||||||
display: flex;
|
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 { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { KnowledgeToFeed } from "@/app/chat/[chatId]/components/ActionsBar/components";
|
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 { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||||
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
|
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
|
||||||
|
import { createHandleGetButtonProps } from "@/lib/helpers/handleConnectionButtons";
|
||||||
|
|
||||||
import styles from "./UploadDocumentModal.module.scss";
|
import styles from "./UploadDocumentModal.module.scss";
|
||||||
import { useAddKnowledge } from "./hooks/useAddKnowledge";
|
import { useAddKnowledge } from "./hooks/useAddKnowledge";
|
||||||
@ -17,10 +20,29 @@ export const UploadDocumentModal = (): JSX.Element => {
|
|||||||
const { currentBrain } = useBrainContext();
|
const { currentBrain } = useBrainContext();
|
||||||
const { feedBrain } = useAddKnowledge();
|
const { feedBrain } = useAddKnowledge();
|
||||||
const [feeding, setFeeding] = useState<boolean>(false);
|
const [feeding, setFeeding] = useState<boolean>(false);
|
||||||
|
const {
|
||||||
|
currentSyncId,
|
||||||
|
setCurrentSyncId,
|
||||||
|
openedConnections,
|
||||||
|
setOpenedConnections,
|
||||||
|
} = useFromConnectionsContext();
|
||||||
|
const [currentConnection, setCurrentConnection] = useState<
|
||||||
|
OpenedConnection | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
useKnowledgeToFeedContext();
|
useKnowledgeToFeedContext();
|
||||||
const { t } = useTranslation(["knowledge"]);
|
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 () => {
|
const handleFeedBrain = async () => {
|
||||||
setFeeding(true);
|
setFeeding(true);
|
||||||
await feedBrain();
|
await feedBrain();
|
||||||
@ -28,6 +50,23 @@ export const UploadDocumentModal = (): JSX.Element => {
|
|||||||
setShouldDisplayFeedCard(false);
|
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) {
|
if (!shouldDisplayFeedCard) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
@ -43,16 +82,44 @@ export const UploadDocumentModal = (): JSX.Element => {
|
|||||||
>
|
>
|
||||||
<div className={styles.knowledge_modal}>
|
<div className={styles.knowledge_modal}>
|
||||||
<KnowledgeToFeed />
|
<KnowledgeToFeed />
|
||||||
<div className={styles.button}>
|
<div
|
||||||
<QuivrButton
|
className={`${styles.buttons} ${
|
||||||
label="Feed Brain"
|
!currentSyncId ? styles.standalone : ""
|
||||||
color="primary"
|
}`}
|
||||||
iconName="add"
|
>
|
||||||
onClick={handleFeedBrain}
|
{!!currentSyncId && (
|
||||||
disabled={knowledgeToFeed.length === 0 || !currentBrain}
|
<QuivrButton
|
||||||
isLoading={feeding}
|
label="Back to connections"
|
||||||
important={true}
|
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>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { useChatApi } from "@/lib/api/chat/useChatApi";
|
||||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||||
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
|
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
|
||||||
@ -25,6 +26,7 @@ export const useFeedBrain = ({
|
|||||||
useKnowledgeToFeedContext();
|
useKnowledgeToFeedContext();
|
||||||
const [hasPendingRequests, setHasPendingRequests] = useState(false);
|
const [hasPendingRequests, setHasPendingRequests] = useState(false);
|
||||||
const { handleFeedBrain } = useFeedBrainHandler();
|
const { handleFeedBrain } = useFeedBrainHandler();
|
||||||
|
const { openedConnections } = useFromConnectionsContext();
|
||||||
|
|
||||||
const { createChat, deleteChat } = useChatApi();
|
const { createChat, deleteChat } = useChatApi();
|
||||||
|
|
||||||
@ -39,7 +41,7 @@ export const useFeedBrain = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (knowledgeToFeed.length === 0) {
|
if (knowledgeToFeed.length === 0 && !openedConnections.length) {
|
||||||
publish({
|
publish({
|
||||||
variant: "danger",
|
variant: "danger",
|
||||||
text: t("addFiles"),
|
text: t("addFiles"),
|
||||||
@ -55,12 +57,11 @@ export const useFeedBrain = ({
|
|||||||
dispatchHasPendingRequests?.();
|
dispatchHasPendingRequests?.();
|
||||||
closeFeedInput?.();
|
closeFeedInput?.();
|
||||||
setHasPendingRequests(true);
|
setHasPendingRequests(true);
|
||||||
setShouldDisplayFeedCard(false);
|
|
||||||
await handleFeedBrain({
|
await handleFeedBrain({
|
||||||
brainId,
|
brainId,
|
||||||
chatId: currentChatId,
|
chatId: currentChatId,
|
||||||
});
|
});
|
||||||
|
setShouldDisplayFeedCard(false);
|
||||||
setKnowledgeToFeed([]);
|
setKnowledgeToFeed([]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
publish({
|
publish({
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { UUID } from "crypto";
|
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 { useKnowledgeToFeedInput } from "@/lib/components/KnowledgeToFeedInput/hooks/useKnowledgeToFeedInput.ts";
|
||||||
import { useKnowledgeToFeedFilesAndUrls } from "@/lib/hooks/useKnowledgeToFeed";
|
import { useKnowledgeToFeedFilesAndUrls } from "@/lib/hooks/useKnowledgeToFeed";
|
||||||
import { useOnboarding } from "@/lib/hooks/useOnboarding";
|
import { useOnboarding } from "@/lib/hooks/useOnboarding";
|
||||||
@ -13,6 +15,13 @@ export const useFeedBrainHandler = () => {
|
|||||||
const { files, urls } = useKnowledgeToFeedFilesAndUrls();
|
const { files, urls } = useKnowledgeToFeedFilesAndUrls();
|
||||||
const { crawlWebsiteHandler, uploadFileHandler } = useKnowledgeToFeedInput();
|
const { crawlWebsiteHandler, uploadFileHandler } = useKnowledgeToFeedInput();
|
||||||
const { updateOnboarding, onboarding } = useOnboarding();
|
const { updateOnboarding, onboarding } = useOnboarding();
|
||||||
|
const {
|
||||||
|
syncFiles,
|
||||||
|
getActiveSyncsForBrain,
|
||||||
|
deleteActiveSync,
|
||||||
|
updateActiveSync,
|
||||||
|
} = useSync();
|
||||||
|
const { openedConnections } = useFromConnectionsContext();
|
||||||
|
|
||||||
const updateOnboardingA = async () => {
|
const updateOnboardingA = async () => {
|
||||||
if (onboarding.onboarding_a) {
|
if (onboarding.onboarding_a) {
|
||||||
@ -33,6 +42,26 @@ export const useFeedBrainHandler = () => {
|
|||||||
crawlWebsiteHandler(url, brainId, chatId)
|
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([
|
await Promise.all([
|
||||||
...uploadPromises,
|
...uploadPromises,
|
||||||
...crawlPromises,
|
...crawlPromises,
|
||||||
|
@ -12,9 +12,22 @@
|
|||||||
height: 16px;
|
height: 16px;
|
||||||
border: 1px solid var(--border-2);
|
border: 1px solid var(--border-2);
|
||||||
border-radius: Radius.$small;
|
border-radius: Radius.$small;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
&.filled {
|
&.filled {
|
||||||
background-color: var(--primary-0);
|
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 styles from "./Checkbox.module.scss";
|
||||||
|
|
||||||
|
import { Icon } from "../Icon/Icon";
|
||||||
|
import Tooltip from "../Tooltip/Tooltip";
|
||||||
|
|
||||||
interface CheckboxProps {
|
interface CheckboxProps {
|
||||||
label: string;
|
label?: string;
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
setChecked: (value: boolean) => void;
|
setChecked: (value: boolean) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
tooltip?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Checkbox = ({
|
export const Checkbox = ({
|
||||||
label,
|
label,
|
||||||
checked,
|
checked,
|
||||||
setChecked,
|
setChecked,
|
||||||
|
disabled,
|
||||||
|
tooltip,
|
||||||
}: CheckboxProps): JSX.Element => {
|
}: CheckboxProps): JSX.Element => {
|
||||||
const [currentChecked, setCurrentChecked] = useState<boolean>(checked);
|
const [currentChecked, setCurrentChecked] = useState<boolean>(checked);
|
||||||
|
|
||||||
@ -19,18 +26,31 @@ export const Checkbox = ({
|
|||||||
setCurrentChecked(checked);
|
setCurrentChecked(checked);
|
||||||
}, [checked]);
|
}, [checked]);
|
||||||
|
|
||||||
return (
|
const checkboxElement = (
|
||||||
<div
|
<div
|
||||||
className={styles.checkbox_wrapper}
|
className={`${styles.checkbox_wrapper} ${
|
||||||
onClick={() => {
|
disabled ? styles.disabled : ""
|
||||||
setChecked(!currentChecked);
|
}`}
|
||||||
setCurrentChecked(!currentChecked);
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (!disabled) {
|
||||||
|
setChecked(!currentChecked);
|
||||||
|
setCurrentChecked(!currentChecked);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`${styles.checkbox} ${currentChecked ? styles.filled : ""}`}
|
className={`${styles.checkbox} ${currentChecked ? styles.filled : ""}`}
|
||||||
></div>
|
>
|
||||||
<span>{label}</span>
|
{currentChecked && <Icon name="check" size="tiny" color="white" />}
|
||||||
|
</div>
|
||||||
|
{label && <span>{label}</span>}
|
||||||
</div>
|
</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";
|
@use "@/styles/IconSizes.module.scss";
|
||||||
|
|
||||||
|
.tiny {
|
||||||
|
min-width: IconSizes.$tiny;
|
||||||
|
min-height: IconSizes.$tiny;
|
||||||
|
max-width: IconSizes.$tiny;
|
||||||
|
max-height: IconSizes.$tiny;
|
||||||
|
}
|
||||||
|
|
||||||
.small {
|
.small {
|
||||||
min-width: IconSizes.$small;
|
min-width: IconSizes.$small;
|
||||||
min-height: IconSizes.$small;
|
min-height: IconSizes.$small;
|
||||||
|
@ -23,7 +23,6 @@
|
|||||||
cursor: auto;
|
cursor: auto;
|
||||||
box-shadow: BoxShadow.$medium;
|
box-shadow: BoxShadow.$medium;
|
||||||
max-width: 90vw;
|
max-width: 90vw;
|
||||||
overflow: scroll;
|
|
||||||
width: 35vw;
|
width: 35vw;
|
||||||
height: 80vh;
|
height: 80vh;
|
||||||
|
|
||||||
@ -32,8 +31,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.big {
|
&.big {
|
||||||
width: 40vw;
|
width: 50vw;
|
||||||
height: 90vh;
|
height: 95vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.white {
|
&.white {
|
||||||
|
@ -14,11 +14,18 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
background-color: var(--background-0);
|
background-color: var(--background-0);
|
||||||
|
height: fit-content;
|
||||||
|
|
||||||
&.hidden {
|
&.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
padding-inline: Spacings.$spacing03;
|
||||||
|
padding-block: Spacings.$spacing01;
|
||||||
|
font-size: Typography.$tiny;
|
||||||
|
}
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
border-color: var(--border-2);
|
border-color: var(--border-2);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
@ -17,6 +17,7 @@ export const QuivrButton = ({
|
|||||||
disabled,
|
disabled,
|
||||||
hidden,
|
hidden,
|
||||||
important,
|
important,
|
||||||
|
small,
|
||||||
}: ButtonType): JSX.Element => {
|
}: ButtonType): JSX.Element => {
|
||||||
const [hovered, setHovered] = useState<boolean>(false);
|
const [hovered, setHovered] = useState<boolean>(false);
|
||||||
const { isDarkMode } = useUserSettingsContext();
|
const { isDarkMode } = useUserSettingsContext();
|
||||||
@ -36,12 +37,33 @@ export const QuivrButton = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getIconColor = () => {
|
const getIconColor = () => {
|
||||||
|
let iconColor = color;
|
||||||
if (hovered || (important && !disabled)) {
|
if (hovered || (important && !disabled)) {
|
||||||
return "white";
|
iconColor = "white";
|
||||||
} else if (disabled) {
|
} 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 {
|
} 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 : ""}
|
${hidden ? styles.hidden : ""}
|
||||||
${important ? styles.important : ""}
|
${important ? styles.important : ""}
|
||||||
${disabled ? styles.disabled : ""}
|
${disabled ? styles.disabled : ""}
|
||||||
|
${small ? styles.small : ""}
|
||||||
`}
|
`}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
>
|
>
|
||||||
<div className={styles.icon_label}>
|
<div className={styles.icon_label}>
|
||||||
{!isLoading ? (
|
{renderIcon()}
|
||||||
<Icon
|
|
||||||
name={iconName}
|
|
||||||
size="normal"
|
|
||||||
color={getIconColor()}
|
|
||||||
handleHover={false}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<LoaderIcon
|
|
||||||
color={hovered || important ? "white" : disabled ? "grey" : color}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<span className={styles.label}>{label}</span>
|
<span className={styles.label}>{label}</span>
|
||||||
</div>
|
</div>
|
||||||
</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/Radius.module.scss";
|
||||||
@use "@/styles/ScreenSizes.module.scss";
|
@use "@/styles/ScreenSizes.module.scss";
|
||||||
@use "@/styles/Spacings.module.scss";
|
@use "@/styles/Spacings.module.scss";
|
||||||
|
@use "@/styles/Typography.module.scss";
|
||||||
|
|
||||||
.tabs_container {
|
.tabs_container {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -37,5 +38,23 @@
|
|||||||
display: none;
|
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"
|
: "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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -24,7 +24,7 @@ export const TextAreaInput = ({
|
|||||||
<textarea
|
<textarea
|
||||||
className={styles.text_area_input}
|
className={styles.text_area_input}
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
rows={5}
|
rows={4}
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
placeholder={label}
|
placeholder={label}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
@use "@/styles/Spacings.module.scss";
|
@use "@/styles/Spacings.module.scss";
|
||||||
|
@use "@/styles/Typography.module.scss";
|
||||||
|
|
||||||
.text_button_wrapper {
|
.text_button_wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -12,6 +13,11 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
gap: Spacings.$spacing02;
|
||||||
|
font-size: Typography.$tiny;
|
||||||
|
}
|
||||||
|
|
||||||
.black {
|
.black {
|
||||||
color: var(--text-3);
|
color: var(--text-3);
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
import { iconList } from "@/lib/helpers/iconList";
|
import { iconList } from "@/lib/helpers/iconList";
|
||||||
import { Color } from "@/lib/types/Colors";
|
import { Color } from "@/lib/types/Colors";
|
||||||
|
|
||||||
@ -9,20 +11,29 @@ interface TextButtonProps {
|
|||||||
iconName?: keyof typeof iconList;
|
iconName?: keyof typeof iconList;
|
||||||
label: string;
|
label: string;
|
||||||
color: Color;
|
color: Color;
|
||||||
onClick?: () => void;
|
onClick?: () => void | Promise<void>;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
small?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TextButton = (props: TextButtonProps): JSX.Element => {
|
export const TextButton = (props: TextButtonProps): JSX.Element => {
|
||||||
|
const [hovered, setHovered] = useState<boolean>(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${styles.text_button_wrapper} ${
|
className={`${styles.text_button_wrapper} ${
|
||||||
props.disabled ? styles.disabled : ""
|
props.disabled ? styles.disabled : ""
|
||||||
}`}
|
} ${props.small ? styles.small : ""}`}
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
|
onMouseEnter={() => setHovered(true)}
|
||||||
|
onMouseLeave={() => setHovered(false)}
|
||||||
>
|
>
|
||||||
{!!props.iconName && (
|
{!!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>
|
<span className={styles[props.color]}>{props.label}</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -24,9 +24,18 @@ export const useKnowledgeToFeedContext = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const removeAllKnowledgeToFeed = () => {
|
||||||
|
context?.setKnowledgeToFeed([]);
|
||||||
|
};
|
||||||
|
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
throw new Error("useKnowledge must be used inside KnowledgeToFeedProvider");
|
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,
|
FaCheckCircle,
|
||||||
FaDiscord,
|
FaDiscord,
|
||||||
FaFileAlt,
|
FaFileAlt,
|
||||||
|
FaFolder,
|
||||||
FaGithub,
|
FaGithub,
|
||||||
FaKey,
|
FaKey,
|
||||||
FaLinkedin,
|
FaLinkedin,
|
||||||
FaMoon,
|
|
||||||
FaQuestionCircle,
|
FaQuestionCircle,
|
||||||
FaRegFileAlt,
|
FaRegFileAlt,
|
||||||
FaRegKeyboard,
|
FaRegKeyboard,
|
||||||
@ -24,7 +24,6 @@ import {
|
|||||||
FaRegThumbsDown,
|
FaRegThumbsDown,
|
||||||
FaRegThumbsUp,
|
FaRegThumbsUp,
|
||||||
FaRegUserCircle,
|
FaRegUserCircle,
|
||||||
FaSun,
|
|
||||||
FaTwitter,
|
FaTwitter,
|
||||||
FaUnlock,
|
FaUnlock,
|
||||||
} from "react-icons/fa";
|
} from "react-icons/fa";
|
||||||
@ -38,13 +37,14 @@ import {
|
|||||||
IoIosRadio,
|
IoIosRadio,
|
||||||
IoMdClose,
|
IoMdClose,
|
||||||
IoMdLogOut,
|
IoMdLogOut,
|
||||||
|
IoMdSettings,
|
||||||
|
IoMdSync,
|
||||||
} from "react-icons/io";
|
} from "react-icons/io";
|
||||||
import {
|
import {
|
||||||
IoArrowUpCircleOutline,
|
IoArrowUpCircleOutline,
|
||||||
IoCloudDownloadOutline,
|
IoCloudDownloadOutline,
|
||||||
IoFootsteps,
|
IoFootsteps,
|
||||||
IoHomeOutline,
|
IoHomeOutline,
|
||||||
IoSettingsSharp,
|
|
||||||
IoShareSocial,
|
IoShareSocial,
|
||||||
IoWarningOutline,
|
IoWarningOutline,
|
||||||
} from "react-icons/io5";
|
} from "react-icons/io5";
|
||||||
@ -63,6 +63,7 @@ import {
|
|||||||
} from "react-icons/lu";
|
} from "react-icons/lu";
|
||||||
import {
|
import {
|
||||||
MdAlternateEmail,
|
MdAlternateEmail,
|
||||||
|
MdDarkMode,
|
||||||
MdDashboardCustomize,
|
MdDashboardCustomize,
|
||||||
MdDeleteOutline,
|
MdDeleteOutline,
|
||||||
MdDynamicFeed,
|
MdDynamicFeed,
|
||||||
@ -70,6 +71,7 @@ import {
|
|||||||
MdLink,
|
MdLink,
|
||||||
MdMarkEmailRead,
|
MdMarkEmailRead,
|
||||||
MdMarkEmailUnread,
|
MdMarkEmailUnread,
|
||||||
|
MdOutlineLightMode,
|
||||||
MdOutlineModeEditOutline,
|
MdOutlineModeEditOutline,
|
||||||
MdUnfoldLess,
|
MdUnfoldLess,
|
||||||
MdUnfoldMore,
|
MdUnfoldMore,
|
||||||
@ -110,6 +112,7 @@ export const iconList: { [name: string]: IconType } = {
|
|||||||
fileSelected: FaFileAlt,
|
fileSelected: FaFileAlt,
|
||||||
flag: CiFlag1,
|
flag: CiFlag1,
|
||||||
fold: MdUnfoldLess,
|
fold: MdUnfoldLess,
|
||||||
|
folder: FaFolder,
|
||||||
followUp: IoArrowUpCircleOutline,
|
followUp: IoArrowUpCircleOutline,
|
||||||
github: FaGithub,
|
github: FaGithub,
|
||||||
goal: LuGoal,
|
goal: LuGoal,
|
||||||
@ -124,7 +127,7 @@ export const iconList: { [name: string]: IconType } = {
|
|||||||
linkedin: FaLinkedin,
|
linkedin: FaLinkedin,
|
||||||
loader: AiOutlineLoading3Quarters,
|
loader: AiOutlineLoading3Quarters,
|
||||||
logout: IoMdLogOut,
|
logout: IoMdLogOut,
|
||||||
moon: FaMoon,
|
moon: MdDarkMode,
|
||||||
notifications: IoIosNotifications,
|
notifications: IoIosNotifications,
|
||||||
office: HiBuildingOffice,
|
office: HiBuildingOffice,
|
||||||
options: SlOptions,
|
options: SlOptions,
|
||||||
@ -136,12 +139,13 @@ export const iconList: { [name: string]: IconType } = {
|
|||||||
read: MdMarkEmailRead,
|
read: MdMarkEmailRead,
|
||||||
robot: LiaRobotSolid,
|
robot: LiaRobotSolid,
|
||||||
search: LuSearch,
|
search: LuSearch,
|
||||||
settings: IoSettingsSharp,
|
settings: IoMdSettings,
|
||||||
share: IoShareSocial,
|
share: IoShareSocial,
|
||||||
software: CgSoftwareDownload,
|
software: CgSoftwareDownload,
|
||||||
star: FaRegStar,
|
star: FaRegStar,
|
||||||
step: IoFootsteps,
|
step: IoFootsteps,
|
||||||
sun: FaSun,
|
sun: MdOutlineLightMode,
|
||||||
|
sync: IoMdSync,
|
||||||
thumbsDown: FaRegThumbsDown,
|
thumbsDown: FaRegThumbsDown,
|
||||||
thumbsUp: FaRegThumbsUp,
|
thumbsUp: FaRegThumbsUp,
|
||||||
twitter: FaTwitter,
|
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;
|
disabled?: boolean;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
important?: boolean;
|
important?: boolean;
|
||||||
|
small?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -6,4 +6,5 @@ export interface Tab {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
iconName: keyof typeof iconList;
|
iconName: keyof typeof iconList;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
badge?: number;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
$tiny: 10px;
|
||||||
$small: 14px;
|
$small: 14px;
|
||||||
$normal: 18px;
|
$normal: 18px;
|
||||||
$large: 24px;
|
$large: 24px;
|
||||||
|
@ -10,12 +10,12 @@
|
|||||||
|
|
||||||
@mixin H2 {
|
@mixin H2 {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 18px;
|
font-size: $large;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin H3 {
|
@mixin H3 {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 16px;
|
font-size: $medium;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin EllipsisOverflow {
|
@mixin EllipsisOverflow {
|
||||||
|
Loading…
Reference in New Issue
Block a user