mirror of
https://github.com/QuivrHQ/quivr.git
synced 2024-09-11 14:36:35 +03:00
feat(frontend): Quivr Assistants (#2448)
# 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):
<!--
ELLIPSIS_HIDDEN
-->
----
| 🚀 This description was created by
[Ellipsis](https://www.ellipsis.dev) for commit
3e95f1a5aa
|
|--------|
### Summary:
This PR introduces the 'Quivr Assistants' feature with new frontend
components and pages, backend routes and DTOs changes, and a minor
update in the `processAssistant` function.
**Key points**:
- Introduced the 'Quivr Assistants' feature with new frontend components
and pages, backend routes and DTOs changes.
- Added new `AssistantModal` component in
`/frontend/app/assistants/AssistantModal/AssistantModal.tsx`.
- Added new `InputsStep` and `OutputsStep` components for handling
assistant inputs and outputs.
- Added new `AssistantModal` page in
`/frontend/app/assistants/page.tsx`.
- Added new API endpoints and types for assistants in
`/frontend/lib/api/assistants/assistants.ts` and
`/frontend/lib/api/assistants/types.ts`.
- Updated backend assistant routes and DTOs in
`backend/modules/assistant/controller/assistant_routes.py`,
`backend/modules/assistant/dto/inputs.py`,
`backend/modules/assistant/ito/difference.py`, and
`backend/modules/assistant/ito/summary.py`.
- Made a minor update in the `processAssistant` function in the
`/frontend/lib/api/assistants/assistants.ts` file.
----
Generated with ❤️ by [ellipsis.dev](https://www.ellipsis.dev)
<!--
ELLIPSIS_HIDDEN
-->
This commit is contained in:
parent
5876bcfcdc
commit
803f304390
@ -5,7 +5,7 @@ from logger import get_logger
|
||||
from middlewares.auth import AuthBearer, get_current_user
|
||||
from modules.assistant.dto.inputs import InputAssistant
|
||||
from modules.assistant.dto.outputs import AssistantOutput
|
||||
from modules.assistant.ito.difference import DifferenceAssistant, difference_inputs
|
||||
from modules.assistant.ito.difference import DifferenceAssistant
|
||||
from modules.assistant.ito.summary import SummaryAssistant, summary_inputs
|
||||
from modules.assistant.service.assistant import Assistant
|
||||
from modules.user.entity.user_identity import UserIdentity
|
||||
@ -43,7 +43,7 @@ async def process_assistant(
|
||||
files: List[UploadFile] = None,
|
||||
current_user: UserIdentity = Depends(get_current_user),
|
||||
):
|
||||
if input.name == "summary":
|
||||
if input.name.lower() == "summary":
|
||||
summary_assistant = SummaryAssistant(
|
||||
input=input, files=files, current_user=current_user
|
||||
)
|
||||
@ -52,7 +52,7 @@ async def process_assistant(
|
||||
return await summary_assistant.process_assistant()
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
elif input.name == "difference":
|
||||
elif input.name.lower() == "difference":
|
||||
difference_assistant = DifferenceAssistant(
|
||||
input=input, files=files, current_user=current_user
|
||||
)
|
||||
|
@ -142,7 +142,7 @@ def difference_inputs():
|
||||
tags=["new"],
|
||||
input_description="Two documents to compare",
|
||||
output_description="The difference between the two documents",
|
||||
icon_url="https://quivr-cms.s3.eu-west-3.amazonaws.com/assistant_summary_434446a2aa.png",
|
||||
icon_url="https://quivr-cms.s3.eu-west-3.amazonaws.com/report_94bea8b918.png",
|
||||
inputs=Inputs(
|
||||
files=[
|
||||
InputFile(
|
||||
|
@ -169,7 +169,7 @@ def summary_inputs():
|
||||
tags=["new"],
|
||||
input_description="One document to summarize",
|
||||
output_description="A summary of the document",
|
||||
icon_url="https://quivr-cms.s3.eu-west-3.amazonaws.com/assistant_summary_434446a2aa.png",
|
||||
icon_url="https://quivr-cms.s3.eu-west-3.amazonaws.com/report_94bea8b918.png",
|
||||
inputs=Inputs(
|
||||
files=[
|
||||
InputFile(
|
||||
|
@ -0,0 +1,29 @@
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
|
||||
.modal_content_container {
|
||||
padding: Spacings.$spacing05;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
|
||||
.modal_content_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing05;
|
||||
|
||||
.message_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
151
frontend/app/assistants/AssistantModal/AssistantModal.tsx
Normal file
151
frontend/app/assistants/AssistantModal/AssistantModal.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { Assistant } from "@/lib/api/assistants/types";
|
||||
import { useAssistants } from "@/lib/api/assistants/useAssistants";
|
||||
import { Stepper } from "@/lib/components/AddBrainModal/components/Stepper/Stepper";
|
||||
import { StepValue } from "@/lib/components/AddBrainModal/types/types";
|
||||
import { MessageInfoBox } from "@/lib/components/ui/MessageInfoBox/MessageInfoBox";
|
||||
import { Modal } from "@/lib/components/ui/Modal/Modal";
|
||||
import QuivrButton from "@/lib/components/ui/QuivrButton/QuivrButton";
|
||||
import { Step } from "@/lib/types/Modal";
|
||||
|
||||
import styles from "./AssistantModal.module.scss";
|
||||
import { InputsStep } from "./InputsStep/InputsStep";
|
||||
import { OutputsStep } from "./OutputsStep/OutputsStep";
|
||||
|
||||
interface AssistantModalProps {
|
||||
isOpen: boolean;
|
||||
setIsOpen: (value: boolean) => void;
|
||||
assistant: Assistant;
|
||||
}
|
||||
|
||||
export const AssistantModal = ({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
assistant,
|
||||
}: AssistantModalProps): JSX.Element => {
|
||||
const steps: Step[] = [
|
||||
{
|
||||
label: "Inputs",
|
||||
value: "FIRST_STEP",
|
||||
},
|
||||
{
|
||||
label: "Outputs",
|
||||
value: "SECOND_STEP",
|
||||
},
|
||||
];
|
||||
const [currentStep, setCurrentStep] = useState<StepValue>("FIRST_STEP");
|
||||
const [emailOutput, setEmailOutput] = useState<boolean>(true);
|
||||
const [brainOutput, setBrainOutput] = useState<string>("");
|
||||
const [files, setFiles] = useState<{ key: string; file: File | null }[]>(
|
||||
assistant.inputs.files.map((fileInput) => ({
|
||||
key: fileInput.key,
|
||||
file: null,
|
||||
}))
|
||||
);
|
||||
const { processAssistant } = useAssistants();
|
||||
|
||||
const handleFileChange = (file: File, inputKey: string) => {
|
||||
setFiles((prevFiles) =>
|
||||
prevFiles.map((fileObj) =>
|
||||
fileObj.key === inputKey ? { ...fileObj, file } : fileObj
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleSetIsOpen = (value: boolean) => {
|
||||
if (!value) {
|
||||
setCurrentStep("FIRST_STEP");
|
||||
}
|
||||
setIsOpen(value);
|
||||
};
|
||||
|
||||
const handleProcessAssistant = async () => {
|
||||
handleSetIsOpen(false);
|
||||
await processAssistant(
|
||||
{
|
||||
name: assistant.name,
|
||||
inputs: {
|
||||
files: files.map((file) => ({
|
||||
key: file.key,
|
||||
value: (file.file as File).name,
|
||||
})),
|
||||
urls: [],
|
||||
texts: [],
|
||||
},
|
||||
outputs: {
|
||||
email: {
|
||||
activated: emailOutput,
|
||||
},
|
||||
brain: {
|
||||
activated: brainOutput !== "",
|
||||
value: brainOutput,
|
||||
},
|
||||
},
|
||||
},
|
||||
files.map((file) => file.file as File)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={assistant.name}
|
||||
desc={assistant.description}
|
||||
isOpen={isOpen}
|
||||
setOpen={handleSetIsOpen}
|
||||
size="big"
|
||||
CloseTrigger={<div />}
|
||||
>
|
||||
<div className={styles.modal_content_container}>
|
||||
<div className={styles.modal_content_wrapper}>
|
||||
<Stepper steps={steps} currentStep={currentStep} />
|
||||
{currentStep === "FIRST_STEP" ? (
|
||||
<MessageInfoBox type="tutorial">
|
||||
<div className={styles.message_wrapper}>
|
||||
<span className={styles.title}>Expected Input</span>
|
||||
{assistant.input_description}
|
||||
</div>
|
||||
</MessageInfoBox>
|
||||
) : (
|
||||
<MessageInfoBox type="tutorial">
|
||||
<div className={styles.message_wrapper}>
|
||||
<span className={styles.title}>Output</span>
|
||||
{assistant.output_description}
|
||||
</div>
|
||||
</MessageInfoBox>
|
||||
)}
|
||||
{currentStep === "FIRST_STEP" ? (
|
||||
<InputsStep
|
||||
inputs={assistant.inputs}
|
||||
onFileChange={handleFileChange}
|
||||
/>
|
||||
) : (
|
||||
<OutputsStep
|
||||
setEmailOutput={setEmailOutput}
|
||||
setBrainOutput={setBrainOutput}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.button}>
|
||||
{currentStep === "FIRST_STEP" ? (
|
||||
<QuivrButton
|
||||
label="Next"
|
||||
color="primary"
|
||||
iconName="chevronRight"
|
||||
onClick={() => setCurrentStep("SECOND_STEP")}
|
||||
disabled={!!files.find((file) => !file.file)}
|
||||
/>
|
||||
) : (
|
||||
<QuivrButton
|
||||
label="Process"
|
||||
color="primary"
|
||||
iconName="chevronRight"
|
||||
onClick={() => handleProcessAssistant()}
|
||||
disabled={!emailOutput && brainOutput === ""}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@ -0,0 +1,28 @@
|
||||
import { capitalCase } from "change-case";
|
||||
|
||||
import { AssistantInputs } from "@/lib/api/assistants/types";
|
||||
import { FileInput } from "@/lib/components/ui/FileInput/FileInput";
|
||||
|
||||
interface InputsStepProps {
|
||||
inputs: AssistantInputs;
|
||||
onFileChange: (file: File, inputKey: string) => void; //
|
||||
}
|
||||
|
||||
export const InputsStep = ({
|
||||
inputs,
|
||||
onFileChange,
|
||||
}: InputsStepProps): JSX.Element => {
|
||||
return (
|
||||
<div>
|
||||
{inputs.files.map((fileInput) => (
|
||||
<FileInput
|
||||
key={fileInput.key}
|
||||
label={capitalCase(fileInput.key)}
|
||||
icon="file"
|
||||
acceptedFileTypes={fileInput.allowed_extensions}
|
||||
onFileChange={(file) => onFileChange(file, fileInput.key)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,16 @@
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
|
||||
.outputs_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing03;
|
||||
|
||||
.message_wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.brain_selector {
|
||||
padding-block: Spacings.$spacing02;
|
||||
max-width: 250px;
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { formatMinimalBrainsToSelectComponentInput } from "@/app/chat/[chatId]/components/ActionsBar/components/KnowledgeToFeed/utils/formatMinimalBrainsToSelectComponentInput";
|
||||
import { Checkbox } from "@/lib/components/ui/Checkbox/Checkbox";
|
||||
import { MessageInfoBox } from "@/lib/components/ui/MessageInfoBox/MessageInfoBox";
|
||||
import { SingleSelector } from "@/lib/components/ui/SingleSelector/SingleSelector";
|
||||
import { requiredRolesForUpload } from "@/lib/config/upload";
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
|
||||
import styles from "./OutputsStep.module.scss";
|
||||
|
||||
interface OutputsStepProps {
|
||||
setEmailOutput: (value: boolean) => void;
|
||||
setBrainOutput: (value: string) => void;
|
||||
}
|
||||
|
||||
export const OutputsStep = ({
|
||||
setEmailOutput,
|
||||
setBrainOutput,
|
||||
}: OutputsStepProps): JSX.Element => {
|
||||
const [existingBrainChecked, setExistingBrainChecked] =
|
||||
useState<boolean>(false);
|
||||
const [selectedBrainId, setSelectedBrainId] = useState<string>("");
|
||||
const { allBrains } = useBrainContext();
|
||||
|
||||
const brainsWithUploadRights = formatMinimalBrainsToSelectComponentInput(
|
||||
useMemo(
|
||||
() =>
|
||||
allBrains.filter(
|
||||
(brain) =>
|
||||
requiredRolesForUpload.includes(brain.role) && !!brain.max_files
|
||||
),
|
||||
[allBrains]
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.outputs_wrapper}>
|
||||
<MessageInfoBox type="info">
|
||||
It can take a few minutes to process.
|
||||
</MessageInfoBox>
|
||||
<Checkbox
|
||||
label="Receive the results by Email"
|
||||
checked={true}
|
||||
setChecked={setEmailOutput}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Upload the results on an existing Brain"
|
||||
checked={existingBrainChecked}
|
||||
setChecked={() => {
|
||||
if (existingBrainChecked) {
|
||||
setBrainOutput("");
|
||||
setSelectedBrainId("");
|
||||
}
|
||||
setExistingBrainChecked(!existingBrainChecked);
|
||||
}}
|
||||
/>
|
||||
{existingBrainChecked && (
|
||||
<div className={styles.brain_selector}>
|
||||
<SingleSelector
|
||||
options={brainsWithUploadRights}
|
||||
onChange={(brain) => {
|
||||
setBrainOutput(brain);
|
||||
setSelectedBrainId(brain);
|
||||
}}
|
||||
selectedOption={
|
||||
selectedBrainId
|
||||
? {
|
||||
value: selectedBrainId,
|
||||
label: allBrains.find(
|
||||
(brain) => brain.id === selectedBrainId
|
||||
)?.name as string,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
placeholder="Select a brain"
|
||||
iconName="brain"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
14
frontend/app/assistants/page.module.scss
Normal file
14
frontend/app/assistants/page.module.scss
Normal file
@ -0,0 +1,14 @@
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
|
||||
.content_wrapper {
|
||||
padding: Spacings.$spacing06;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing05;
|
||||
|
||||
.assistants_grid {
|
||||
display: flex;
|
||||
gap: Spacings.$spacing03;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
94
frontend/app/assistants/page.tsx
Normal file
94
frontend/app/assistants/page.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
"use client";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { Assistant } from "@/lib/api/assistants/types";
|
||||
import { useAssistants } from "@/lib/api/assistants/useAssistants";
|
||||
import PageHeader from "@/lib/components/PageHeader/PageHeader";
|
||||
import { BrainCard } from "@/lib/components/ui/BrainCard/BrainCard";
|
||||
import { MessageInfoBox } from "@/lib/components/ui/MessageInfoBox/MessageInfoBox";
|
||||
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
||||
import { redirectToLogin } from "@/lib/router/redirectToLogin";
|
||||
|
||||
import { AssistantModal } from "./AssistantModal/AssistantModal";
|
||||
import styles from "./page.module.scss";
|
||||
|
||||
const Search = (): JSX.Element => {
|
||||
const pathname = usePathname();
|
||||
const { session } = useSupabase();
|
||||
const [assistants, setAssistants] = useState<Assistant[]>([]);
|
||||
const [assistantModalOpened, setAssistantModalOpened] =
|
||||
useState<boolean>(false);
|
||||
const [currentAssistant, setCurrentAssistant] = useState<Assistant | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const { getAssistants } = useAssistants();
|
||||
|
||||
useEffect(() => {
|
||||
if (session === null) {
|
||||
redirectToLogin();
|
||||
}
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const res = await getAssistants();
|
||||
if (res) {
|
||||
setAssistants(res);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
})();
|
||||
}, [pathname, session]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.page_header}>
|
||||
<PageHeader
|
||||
iconName="assistant"
|
||||
label="Quivr Assistants"
|
||||
buttons={[]}
|
||||
/>
|
||||
<div className={styles.content_wrapper}>
|
||||
<div className={styles.message_wrapper}>
|
||||
<MessageInfoBox type="info">
|
||||
<span>
|
||||
Quivr assistants are AI-driven agents that apply specific
|
||||
processes to an input in order to generate a usable output.{" "}
|
||||
<br></br>This output can be directly uploaded to a digital brain
|
||||
or sent via email.{" "}
|
||||
</span>
|
||||
</MessageInfoBox>
|
||||
</div>
|
||||
<div className={styles.assistants_grid}>
|
||||
{assistants.map((assistant) => {
|
||||
return (
|
||||
<BrainCard
|
||||
tooltip={assistant.description}
|
||||
brainName={assistant.name}
|
||||
tags={assistant.tags}
|
||||
imageUrl={assistant.icon_url}
|
||||
callback={() => {
|
||||
setAssistantModalOpened(true);
|
||||
setCurrentAssistant(assistant);
|
||||
}}
|
||||
key={assistant.name}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{currentAssistant && (
|
||||
<AssistantModal
|
||||
isOpen={assistantModalOpened}
|
||||
setIsOpen={setAssistantModalOpened}
|
||||
assistant={currentAssistant}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Search;
|
38
frontend/lib/api/assistants/assistants.ts
Normal file
38
frontend/lib/api/assistants/assistants.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { AxiosInstance } from "axios";
|
||||
|
||||
import { Assistant, ProcessAssistantRequest } from "./types";
|
||||
|
||||
export const getAssistants = async (
|
||||
axiosInstance: AxiosInstance
|
||||
): Promise<Assistant[] | undefined> => {
|
||||
return (await axiosInstance.get<Assistant[] | undefined>("/assistants")).data;
|
||||
};
|
||||
|
||||
export const processAssistant = async (
|
||||
axiosInstance: AxiosInstance,
|
||||
input: ProcessAssistantRequest,
|
||||
files: File[]
|
||||
): Promise<string | undefined> => {
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append(
|
||||
"input",
|
||||
JSON.stringify({
|
||||
name: input.name,
|
||||
inputs: {
|
||||
files: input.inputs.files,
|
||||
urls: input.inputs.urls,
|
||||
texts: input.inputs.texts,
|
||||
},
|
||||
outputs: input.outputs,
|
||||
})
|
||||
);
|
||||
|
||||
files.forEach((file) => {
|
||||
formData.append("files", file);
|
||||
});
|
||||
|
||||
return (
|
||||
await axiosInstance.post<string | undefined>("/assistant/process", formData)
|
||||
).data;
|
||||
};
|
64
frontend/lib/api/assistants/types.ts
Normal file
64
frontend/lib/api/assistants/types.ts
Normal file
@ -0,0 +1,64 @@
|
||||
interface AssistantInput {
|
||||
key: string;
|
||||
required: boolean;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface FilesInputAssistant extends AssistantInput {
|
||||
allowed_extensions: string[];
|
||||
}
|
||||
|
||||
export interface AssistantInputs {
|
||||
files: FilesInputAssistant[];
|
||||
urls: AssistantInput[];
|
||||
texts: AssistantInput[];
|
||||
}
|
||||
|
||||
interface AssistantOutput {
|
||||
required: boolean;
|
||||
description: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface AssistantOutputs {
|
||||
email: AssistantOutput;
|
||||
brain: AssistantOutput;
|
||||
}
|
||||
|
||||
export interface Assistant {
|
||||
name: string;
|
||||
input_description: string;
|
||||
output_description: string;
|
||||
inputs: AssistantInputs;
|
||||
outputs: AssistantOutputs;
|
||||
tags: string[];
|
||||
icon_url: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface ProcessAssistantRequest {
|
||||
name: string;
|
||||
inputs: {
|
||||
files: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
urls: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
texts: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
};
|
||||
outputs: {
|
||||
email: {
|
||||
activated: boolean;
|
||||
};
|
||||
brain: {
|
||||
activated: boolean;
|
||||
value: string;
|
||||
};
|
||||
};
|
||||
}
|
15
frontend/lib/api/assistants/useAssistants.ts
Normal file
15
frontend/lib/api/assistants/useAssistants.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { useAxios } from "@/lib/hooks";
|
||||
|
||||
import { getAssistants, processAssistant } from "./assistants";
|
||||
import { ProcessAssistantRequest } from "./types";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useAssistants = () => {
|
||||
const { axiosInstance } = useAxios();
|
||||
|
||||
return {
|
||||
getAssistants: async () => getAssistants(axiosInstance),
|
||||
processAssistant: async (input: ProcessAssistantRequest, files: File[]) =>
|
||||
processAssistant(axiosInstance, input, files),
|
||||
};
|
||||
};
|
@ -12,11 +12,13 @@ import { BrainMainInfosStep } from "./components/BrainMainInfosStep/BrainMainInf
|
||||
import { BrainTypeSelectionStep } from "./components/BrainTypeSelectionStep/BrainTypeSelectionStep";
|
||||
import { CreateBrainStep } from "./components/CreateBrainStep/CreateBrainStep";
|
||||
import { Stepper } from "./components/Stepper/Stepper";
|
||||
import { useBrainCreationSteps } from "./hooks/useBrainCreationSteps";
|
||||
import { CreateBrainProps } from "./types/types";
|
||||
|
||||
export const AddBrainModal = (): JSX.Element => {
|
||||
const { t } = useTranslation(["translation", "brain", "config"]);
|
||||
const { userIdentityData } = useUserData();
|
||||
const { currentStep, steps } = useBrainCreationSteps();
|
||||
|
||||
const {
|
||||
isBrainCreationModalOpened,
|
||||
@ -27,7 +29,7 @@ export const AddBrainModal = (): JSX.Element => {
|
||||
const defaultValues: CreateBrainProps = {
|
||||
...addBrainDefaultValues,
|
||||
setDefault: true,
|
||||
brainCreationStep: "BRAIN_TYPE",
|
||||
brainCreationStep: "FIRST_STEP",
|
||||
};
|
||||
|
||||
const methods = useForm<CreateBrainProps>({
|
||||
@ -51,7 +53,7 @@ export const AddBrainModal = (): JSX.Element => {
|
||||
>
|
||||
<div className={styles.add_brain_modal_container}>
|
||||
<div className={styles.stepper_container}>
|
||||
<Stepper />
|
||||
<Stepper currentStep={currentStep} steps={steps} />
|
||||
</div>
|
||||
<div className={styles.content_wrapper}>
|
||||
<BrainTypeSelectionStep />
|
||||
|
@ -2,6 +2,8 @@ import { createContext, useContext, useState } from "react";
|
||||
|
||||
import { IntegrationBrains } from "@/lib/api/brain/types";
|
||||
|
||||
import { StepValue } from "./types/types";
|
||||
|
||||
interface BrainCreationContextProps {
|
||||
isBrainCreationModalOpened: boolean;
|
||||
setIsBrainCreationModalOpened: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
@ -11,6 +13,8 @@ interface BrainCreationContextProps {
|
||||
setCurrentSelectedBrain: React.Dispatch<
|
||||
React.SetStateAction<IntegrationBrains | undefined>
|
||||
>;
|
||||
currentStep: StepValue;
|
||||
setCurrentStep: React.Dispatch<React.SetStateAction<StepValue>>;
|
||||
}
|
||||
|
||||
export const BrainCreationContext = createContext<
|
||||
@ -27,6 +31,7 @@ export const BrainCreationProvider = ({
|
||||
const [currentSelectedBrain, setCurrentSelectedBrain] =
|
||||
useState<IntegrationBrains>();
|
||||
const [creating, setCreating] = useState<boolean>(false);
|
||||
const [currentStep, setCurrentStep] = useState<StepValue>("FIRST_STEP");
|
||||
|
||||
return (
|
||||
<BrainCreationContext.Provider
|
||||
@ -37,6 +42,8 @@ export const BrainCreationProvider = ({
|
||||
setCreating,
|
||||
currentSelectedBrain,
|
||||
setCurrentSelectedBrain,
|
||||
currentStep,
|
||||
setCurrentStep,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@ -1,5 +1,3 @@
|
||||
@use "@/styles/Radius.module.scss";
|
||||
@use "@/styles/ScreenSizes.module.scss";
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
@use "@/styles/Typography.module.scss";
|
||||
|
||||
@ -18,55 +16,5 @@
|
||||
display: flex;
|
||||
gap: Spacings.$spacing03;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.brain_card_container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing02;
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.tag_wrapper {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.brain_card_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border-radius: Radius.$normal;
|
||||
gap: Spacings.$spacing03;
|
||||
padding: Spacings.$spacing04;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
width: 120px;
|
||||
|
||||
.dark_image {
|
||||
filter: invert(100%);
|
||||
}
|
||||
|
||||
.brain_title {
|
||||
@include Typography.EllipsisOverflow;
|
||||
font-size: Typography.$small;
|
||||
font-weight: 500;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.selected {
|
||||
border-color: var(--primary-0);
|
||||
background-color: var(--background-special-0);
|
||||
|
||||
.brain_title {
|
||||
color: var(--primary-0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,6 @@
|
||||
import { capitalCase } from "change-case";
|
||||
import Image from "next/image";
|
||||
|
||||
import { IntegrationBrains } from "@/lib/api/brain/types";
|
||||
import { BrainCard } from "@/lib/components/ui/BrainCard/BrainCard";
|
||||
import { MessageInfoBox } from "@/lib/components/ui/MessageInfoBox/MessageInfoBox";
|
||||
import { Tag } from "@/lib/components/ui/Tag/Tag";
|
||||
import Tooltip from "@/lib/components/ui/Tooltip/Tooltip";
|
||||
import { useUserSettingsContext } from "@/lib/context/UserSettingsProvider/hooks/useUserSettingsContext";
|
||||
import { useUserData } from "@/lib/hooks/useUserData";
|
||||
|
||||
import styles from "./BrainCatalogue.module.scss";
|
||||
@ -21,7 +16,6 @@ export const BrainCatalogue = ({
|
||||
}): JSX.Element => {
|
||||
const { setCurrentSelectedBrain, currentSelectedBrain } =
|
||||
useBrainCreationContext();
|
||||
const { isDarkMode } = useUserSettingsContext();
|
||||
const { userIdentityData } = useUserData();
|
||||
|
||||
return (
|
||||
@ -45,42 +39,19 @@ export const BrainCatalogue = ({
|
||||
<div className={styles.brains_grid}>
|
||||
{brains.map((brain) => {
|
||||
return (
|
||||
<div
|
||||
key={brain.id}
|
||||
className={`${styles.brain_card_container} ${
|
||||
!userIdentityData?.onboarded && !brain.onboarding_brain
|
||||
? styles.disabled
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
<BrainCard
|
||||
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);
|
||||
}}
|
||||
>
|
||||
<Tooltip tooltip={brain.description}>
|
||||
<div
|
||||
className={`${styles.brain_card_wrapper} ${
|
||||
currentSelectedBrain === brain ? styles.selected : ""
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
className={isDarkMode ? styles.dark_image : ""}
|
||||
src={brain.integration_logo_url}
|
||||
alt={brain.integration_name}
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
<span className={styles.brain_title}>
|
||||
{brain.integration_display_name}
|
||||
</span>
|
||||
<div className={styles.tag_wrapper}>
|
||||
{brain.tags[0] && (
|
||||
<Tag color="primary" name={capitalCase(brain.tags[0])} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
key={brain.id}
|
||||
disabled={!userIdentityData?.onboarded && !brain.onboarding_brain}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
@ -2,11 +2,14 @@ import { Icon } from "@/lib/components/ui/Icon/Icon";
|
||||
|
||||
import styles from "./Stepper.module.scss";
|
||||
|
||||
import { useBrainCreationSteps } from "../../hooks/useBrainCreationSteps";
|
||||
import { StepValue } from "../../types/types";
|
||||
|
||||
export const Stepper = (): JSX.Element => {
|
||||
const { currentStep, steps } = useBrainCreationSteps();
|
||||
interface StepperProps {
|
||||
currentStep: StepValue;
|
||||
steps: { value: string; label: string }[];
|
||||
}
|
||||
|
||||
export const Stepper = ({ currentStep, steps }: StepperProps): JSX.Element => {
|
||||
const currentStepIndex = steps.findIndex(
|
||||
(step) => step.value === currentStep
|
||||
);
|
||||
|
@ -1,35 +1,31 @@
|
||||
import { useEffect } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import {
|
||||
CreateBrainProps,
|
||||
Step,
|
||||
} from "@/lib/components/AddBrainModal/types/types";
|
||||
import { Step } from "@/lib/types/Modal";
|
||||
|
||||
import { useBrainCreationContext } from "../brainCreation-provider";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useBrainCreationSteps = () => {
|
||||
const { t } = useTranslation("brain");
|
||||
const { isBrainCreationModalOpened } = useBrainCreationContext();
|
||||
const { isBrainCreationModalOpened, currentStep, setCurrentStep } =
|
||||
useBrainCreationContext();
|
||||
|
||||
const steps: Step[] = [
|
||||
{
|
||||
label: t("brain_type"),
|
||||
value: "BRAIN_TYPE",
|
||||
value: "FIRST_STEP",
|
||||
},
|
||||
{
|
||||
label: t("brain_params"),
|
||||
value: "BRAIN_PARAMS",
|
||||
value: "SECOND_STEP",
|
||||
},
|
||||
{
|
||||
label: t("resources"),
|
||||
value: "KNOWLEDGE",
|
||||
value: "THIRD_STEP",
|
||||
},
|
||||
];
|
||||
const { watch, setValue } = useFormContext<CreateBrainProps>();
|
||||
const currentStep = watch("brainCreationStep");
|
||||
|
||||
const currentStepIndex = steps.findIndex(
|
||||
(step) => step.value === currentStep
|
||||
);
|
||||
@ -44,7 +40,7 @@ export const useBrainCreationSteps = () => {
|
||||
}
|
||||
const nextStep = steps[currentStepIndex + 1];
|
||||
|
||||
return setValue("brainCreationStep", nextStep.value);
|
||||
return setCurrentStep(nextStep.value);
|
||||
};
|
||||
|
||||
const goToPreviousStep = () => {
|
||||
@ -53,11 +49,11 @@ export const useBrainCreationSteps = () => {
|
||||
}
|
||||
const previousStep = steps[currentStepIndex - 1];
|
||||
|
||||
return setValue("brainCreationStep", previousStep.value);
|
||||
return setCurrentStep(previousStep.value);
|
||||
};
|
||||
|
||||
const goToFirstStep = () => {
|
||||
return setValue("brainCreationStep", steps[0].value);
|
||||
return setCurrentStep(steps[0].value);
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { CreateBrainInput } from "@/lib/api/brain/types";
|
||||
import { iconList } from "@/lib/helpers/iconList";
|
||||
|
||||
const brainCreationSteps = ["BRAIN_TYPE", "BRAIN_PARAMS", "KNOWLEDGE"] as const;
|
||||
const steps = ["FIRST_STEP", "SECOND_STEP", "THIRD_STEP"] as const;
|
||||
|
||||
export type BrainCreationStep = (typeof brainCreationSteps)[number];
|
||||
export type StepValue = (typeof steps)[number];
|
||||
|
||||
export type CreateBrainProps = CreateBrainInput & {
|
||||
setDefault: boolean;
|
||||
brainCreationStep: BrainCreationStep;
|
||||
brainCreationStep: StepValue;
|
||||
};
|
||||
|
||||
export interface BrainType {
|
||||
@ -17,8 +17,3 @@ export interface BrainType {
|
||||
disabled?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export type Step = {
|
||||
label: string;
|
||||
value: BrainCreationStep;
|
||||
};
|
||||
|
@ -11,6 +11,7 @@ import { useUserSettingsContext } from "@/lib/context/UserSettingsProvider/hooks
|
||||
|
||||
import styles from "./Menu.module.scss";
|
||||
import { AnimatedDiv } from "./components/AnimationDiv";
|
||||
import { AssistantsButton } from "./components/AssistantsButton/AssistantsButton";
|
||||
import { DiscussionButton } from "./components/DiscussionButton/DiscussionButton";
|
||||
import { HomeButton } from "./components/HomeButton/HomeButton";
|
||||
import { ProfileButton } from "./components/ProfileButton/ProfileButton";
|
||||
@ -32,7 +33,14 @@ export const Menu = (): JSX.Element => {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const displayedOnPages = ["/chat", "/library", "/studio", "/search", "/user"];
|
||||
const displayedOnPages = [
|
||||
"/assistants",
|
||||
"/chat",
|
||||
"/library",
|
||||
"/search",
|
||||
"studio",
|
||||
"/user",
|
||||
];
|
||||
|
||||
const isMenuDisplayed = displayedOnPages.some((page) =>
|
||||
pathname.includes(page)
|
||||
@ -66,6 +74,7 @@ export const Menu = (): JSX.Element => {
|
||||
<DiscussionButton />
|
||||
<HomeButton />
|
||||
<StudioButton />
|
||||
<AssistantsButton />
|
||||
<ThreadsButton />
|
||||
</div>
|
||||
<div className={styles.block}>
|
||||
|
@ -0,0 +1,21 @@
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import { MenuButton } from "@/lib/components/Menu/components/MenuButton/MenuButton";
|
||||
|
||||
export const AssistantsButton = (): JSX.Element => {
|
||||
const pathname = usePathname() ?? "";
|
||||
const isSelected = pathname.includes("/assistants");
|
||||
|
||||
return (
|
||||
<Link href={`/assistants`}>
|
||||
<MenuButton
|
||||
label="Quivr Assistants"
|
||||
isSelected={isSelected}
|
||||
iconName="assistant"
|
||||
type="open"
|
||||
color="primary"
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
};
|
@ -9,6 +9,7 @@
|
||||
padding: Spacings.$spacing04;
|
||||
padding-left: Spacings.$spacing09;
|
||||
border-bottom: 1px solid var(--border-1);
|
||||
height: 3rem;
|
||||
|
||||
.left {
|
||||
@include Typography.H2;
|
||||
@ -30,7 +31,6 @@
|
||||
.buttons_wrapper {
|
||||
display: flex;
|
||||
gap: Spacings.$spacing03;
|
||||
align-self: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
50
frontend/lib/components/ui/BrainCard/BrainCard.module.scss
Normal file
50
frontend/lib/components/ui/BrainCard/BrainCard.module.scss
Normal file
@ -0,0 +1,50 @@
|
||||
@use "@/styles/Radius.module.scss";
|
||||
@use "@/styles/ScreenSizes.module.scss";
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
@use "@/styles/Typography.module.scss";
|
||||
|
||||
.brain_card_container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: Spacings.$spacing02;
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.brain_card_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border-radius: Radius.$normal;
|
||||
gap: Spacings.$spacing03;
|
||||
padding: Spacings.$spacing04;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
width: 120px;
|
||||
|
||||
.dark_image {
|
||||
filter: invert(100%);
|
||||
}
|
||||
|
||||
.brain_title {
|
||||
@include Typography.EllipsisOverflow;
|
||||
font-size: Typography.$small;
|
||||
font-weight: 500;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.selected {
|
||||
border-color: var(--primary-0);
|
||||
background-color: var(--background-special-0);
|
||||
|
||||
.brain_title {
|
||||
color: var(--primary-0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
65
frontend/lib/components/ui/BrainCard/BrainCard.tsx
Normal file
65
frontend/lib/components/ui/BrainCard/BrainCard.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import { capitalCase } from "change-case";
|
||||
import Image from "next/image";
|
||||
|
||||
import { useUserSettingsContext } from "@/lib/context/UserSettingsProvider/hooks/useUserSettingsContext";
|
||||
|
||||
import styles from "./BrainCard.module.scss";
|
||||
|
||||
import { Tag } from "../Tag/Tag";
|
||||
import Tooltip from "../Tooltip/Tooltip";
|
||||
|
||||
interface BrainCardProps {
|
||||
tooltip: string;
|
||||
selected?: boolean;
|
||||
imageUrl: string;
|
||||
brainName: string;
|
||||
tags: string[];
|
||||
callback: () => void;
|
||||
key: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const BrainCard = ({
|
||||
tooltip,
|
||||
selected,
|
||||
imageUrl,
|
||||
brainName,
|
||||
tags,
|
||||
callback,
|
||||
key,
|
||||
disabled,
|
||||
}: BrainCardProps): JSX.Element => {
|
||||
const { isDarkMode } = useUserSettingsContext();
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className={`${styles.brain_card_container} ${
|
||||
disabled ? styles.disabled : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
callback();
|
||||
}}
|
||||
>
|
||||
<Tooltip tooltip={tooltip}>
|
||||
<div
|
||||
className={`${styles.brain_card_wrapper} ${
|
||||
selected ? styles.selected : ""
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
className={isDarkMode ? styles.dark_image : ""}
|
||||
src={imageUrl}
|
||||
alt={brainName}
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
<span className={styles.brain_title}>{brainName}</span>
|
||||
<div className={styles.tag_wrapper}>
|
||||
{tags[0] && <Tag color="primary" name={capitalCase(tags[0])} />}
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
20
frontend/lib/components/ui/Checkbox/Checkbox.module.scss
Normal file
20
frontend/lib/components/ui/Checkbox/Checkbox.module.scss
Normal file
@ -0,0 +1,20 @@
|
||||
@use "@/styles/Radius.module.scss";
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
|
||||
.checkbox_wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: Spacings.$spacing03;
|
||||
cursor: pointer;
|
||||
|
||||
.checkbox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px solid var(--border-2);
|
||||
border-radius: Radius.$small;
|
||||
|
||||
&.filled {
|
||||
background-color: var(--primary-0);
|
||||
}
|
||||
}
|
||||
}
|
36
frontend/lib/components/ui/Checkbox/Checkbox.tsx
Normal file
36
frontend/lib/components/ui/Checkbox/Checkbox.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import styles from "./Checkbox.module.scss";
|
||||
|
||||
interface CheckboxProps {
|
||||
label: string;
|
||||
checked: boolean;
|
||||
setChecked: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export const Checkbox = ({
|
||||
label,
|
||||
checked,
|
||||
setChecked,
|
||||
}: CheckboxProps): JSX.Element => {
|
||||
const [currentChecked, setCurrentChecked] = useState<boolean>(checked);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentChecked(checked);
|
||||
}, [checked]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.checkbox_wrapper}
|
||||
onClick={() => {
|
||||
setChecked(!currentChecked);
|
||||
setCurrentChecked(!currentChecked);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`${styles.checkbox} ${currentChecked ? styles.filled : ""}`}
|
||||
></div>
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
32
frontend/lib/components/ui/FileInput/FileInput.module.scss
Normal file
32
frontend/lib/components/ui/FileInput/FileInput.module.scss
Normal file
@ -0,0 +1,32 @@
|
||||
@use "@/styles/Radius.module.scss";
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
@use "@/styles/Typography.module.scss";
|
||||
|
||||
.header_wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border: 1px solid var(--border-2);
|
||||
border-radius: Radius.$big;
|
||||
padding: Spacings.$spacing03;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--background-2);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: var(--text-2);
|
||||
font-size: Typography.$small;
|
||||
}
|
||||
}
|
||||
|
||||
.filename {
|
||||
font-size: Typography.$tiny;
|
||||
}
|
||||
|
||||
.error_message {
|
||||
font-size: Typography.$tiny;
|
||||
color: var(--dangerous);
|
||||
}
|
69
frontend/lib/components/ui/FileInput/FileInput.tsx
Normal file
69
frontend/lib/components/ui/FileInput/FileInput.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import { iconList } from "@/lib/helpers/iconList";
|
||||
|
||||
import styles from "./FileInput.module.scss";
|
||||
|
||||
import { FieldHeader } from "../FieldHeader/FieldHeader";
|
||||
import { Icon } from "../Icon/Icon";
|
||||
|
||||
interface FileInputProps {
|
||||
label: string;
|
||||
icon: keyof typeof iconList;
|
||||
onFileChange: (file: File) => void;
|
||||
acceptedFileTypes?: string[];
|
||||
}
|
||||
|
||||
export const FileInput = (props: FileInputProps): JSX.Element => {
|
||||
const [currentFile, setcurrentFile] = useState<File | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
const fileExtension = file.name.split(".").pop();
|
||||
if (props.acceptedFileTypes?.includes(fileExtension || "")) {
|
||||
props.onFileChange(file);
|
||||
setcurrentFile(file);
|
||||
setErrorMessage("");
|
||||
} else {
|
||||
setErrorMessage("Wrong extension");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FieldHeader label={props.label} iconName={props.icon.toString()} />
|
||||
<div className={styles.file_input_wrapper} onClick={handleClick}>
|
||||
<div className={styles.header_wrapper}>
|
||||
<span className={styles.placeholder}>
|
||||
Click here to {currentFile ? "change your" : "upload a"} file
|
||||
</span>
|
||||
<Icon name="upload" size="normal" color="black" />
|
||||
</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
className={styles.file_input}
|
||||
onChange={handleFileChange}
|
||||
accept={props.acceptedFileTypes
|
||||
?.map((type) => `application/${type}`)
|
||||
.join(",")}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
</div>
|
||||
{currentFile && (
|
||||
<span className={styles.filename}>{currentFile.name}</span>
|
||||
)}
|
||||
{errorMessage !== "" && (
|
||||
<span className={styles.error_message}>{errorMessage}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -71,12 +71,13 @@ import {
|
||||
import { PiOfficeChairFill } from "react-icons/pi";
|
||||
import { RiDeleteBackLine, RiHashtag } from "react-icons/ri";
|
||||
import { SlOptions } from "react-icons/sl";
|
||||
import { TbNetwork } from "react-icons/tb";
|
||||
import { TbNetwork, TbRobot } from "react-icons/tb";
|
||||
import { VscGraph } from "react-icons/vsc";
|
||||
|
||||
export const iconList: { [name: string]: IconType } = {
|
||||
add: LuPlusCircle,
|
||||
addWithoutCircle: IoIosAdd,
|
||||
assistant: TbRobot,
|
||||
back: RiDeleteBackLine,
|
||||
brain: LuBrain,
|
||||
brainCircuit: LuBrainCircuit,
|
||||
|
6
frontend/lib/types/Modal.ts
Normal file
6
frontend/lib/types/Modal.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { StepValue } from "../components/AddBrainModal/types/types";
|
||||
|
||||
export type Step = {
|
||||
label: string;
|
||||
value: StepValue;
|
||||
};
|
Loading…
Reference in New Issue
Block a user