Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ __pycache__/
.DS_Store
.idea
.env

databiosphere-findable-ui-47.0.2.tgz
524 changes: 253 additions & 271 deletions package-lock.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const ADORNMENT_TYPE = {
DEFAULT: "default",
SUBMITTABLE: "submittable",
SUBMITTING: "submitting",
} as const;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import styled from "@emotion/styled";
import { ArrowUpwardRounded } from "@mui/icons-material";
import { PALETTE } from "../../../../../../../../styles/common/constants/palette";
import { LoadingIcon } from "../../../../../../../common/CustomIcon/components/LoadingIcon/loadingIcon";

export const StyledLoadingIcon = styled(LoadingIcon)`
&& {
color: ${PALETTE.PRIMARY_MAIN};

.Mui-focused & {
color: ${PALETTE.PRIMARY_MAIN};
}
}
`;

export const StyledArrowUpwardRounded = styled(ArrowUpwardRounded)`
&& {
color: ${PALETTE.INK_LIGHT};

.Mui-focused & {
color: ${PALETTE.INK_LIGHT};
}
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { IconButton } from "@mui/material";
import React from "react";
import { ICON_BUTTON_PROPS } from "../../../../../../../../styles/common/mui/iconButton";
import { SVG_ICON_PROPS } from "../../../../../../../../styles/common/mui/svgIcon";
import { AiIcon } from "../../../../../../../common/CustomIcon/components/AiIcon/aiIcon";
import { ADORNMENT_TYPE } from "./constants";
import {
StyledArrowUpwardRounded,
StyledLoadingIcon,
} from "./endAdornment.styles";
import { EndAdornmentProps } from "./types";

export const EndAdornment = ({
adornmentType,
}: EndAdornmentProps): JSX.Element => {
switch (adornmentType) {
case ADORNMENT_TYPE.SUBMITTING:
return (
<StyledLoadingIcon
color={SVG_ICON_PROPS.COLOR.PRIMARY}
fontSize={SVG_ICON_PROPS.FONT_SIZE.SMALL}
/>
);
case ADORNMENT_TYPE.SUBMITTABLE:
return (
<IconButton
color={ICON_BUTTON_PROPS.COLOR.SECONDARY}
edge={ICON_BUTTON_PROPS.EDGE.END}
size={ICON_BUTTON_PROPS.SIZE.XSMALL}
type="submit"
>
<StyledArrowUpwardRounded fontSize={SVG_ICON_PROPS.FONT_SIZE.SMALL} />
</IconButton>
);
case ADORNMENT_TYPE.DEFAULT:
default:
return <AiIcon />;
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ADORNMENT_TYPE } from "./constants";

export interface EndAdornmentProps {
adornmentType: (typeof ADORNMENT_TYPE)[keyof typeof ADORNMENT_TYPE];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ADORNMENT_TYPE } from "./constants";

/**
* Returns the end adornment type of the input i.e. "default", "submittable", "submitting".
* @param isDirty - Form is dirty.
* @param isSubmiting - Form is submitting.
* @returns End adornment type.
*/
export function getEndAdornmentType(
isDirty: boolean,
isSubmiting: boolean
): (typeof ADORNMENT_TYPE)[keyof typeof ADORNMENT_TYPE] {
if (isSubmiting) return ADORNMENT_TYPE.SUBMITTING;
if (isDirty) return ADORNMENT_TYPE.SUBMITTABLE;
return ADORNMENT_TYPE.DEFAULT;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { OutlinedInputProps } from "@mui/material";
import { TEXT_FIELD_PROPS as MUI_TEXT_FIELD_PROPS } from "../../../../../../styles/common/mui/textField";

export const OUTLINED_INPUT_PROPS: OutlinedInputProps = {
autoComplete: "off",
color: MUI_TEXT_FIELD_PROPS.COLOR.SECONDARY,
fullWidth: true,
id: "query-to-facets",
name: "query-to-facets",
placeholder: "Search all filters...",
size: MUI_TEXT_FIELD_PROPS.SIZE.SMALL,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import styled from "@emotion/styled";
import { OutlinedInput } from "@mui/material";
import { PALETTE } from "../../../../../../styles/common/constants/palette";

export const StyledForm = styled.form`
grid-column: 1 / -1;
`;

export const StyledOutlinedInput = styled(OutlinedInput)`
&:not(:placeholder-shown) {
.MuiOutlinedInput-input {
color: ${PALETTE.INK_MAIN};
}
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React, { FormEvent, useCallback, useState } from "react";
import { useExploreState } from "../../../../../../hooks/useExploreState";
import { ExploreActionKind } from "../../../../../../providers/exploreState";
import { getFormValue } from "../../../../../../utils/form";
import { EndAdornment } from "./components/EndAdornment/endAdornment";
import { getEndAdornmentType } from "./components/EndAdornment/utils";
import { OUTLINED_INPUT_PROPS } from "./constants";
import { StyledForm, StyledOutlinedInput } from "./facetAssistant.styles";
import { mapResponse } from "./utils";

/**
* AI-powered facet assistant component.
* Converts a user query into facet filters (PoC implementation).
*/

export const FacetAssistant = (): JSX.Element => {
const { exploreDispatch } = useExploreState();
const [isDirty, setIsDirty] = useState<boolean>(false);
const [isError, setIsError] = useState<boolean>(false);
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);

const onSubmit = useCallback(
async (e: FormEvent<HTMLFormElement>): Promise<void> => {
e.preventDefault();

// Get the form value.
const formValue = getFormValue(e, "query-to-facets");
if (!formValue) return;

setIsSubmitting(true);
setIsError(false);

try {
const res = await fetch(
"http://localhost:8000/api/v0/facets?mode=llm",
{
body: JSON.stringify({ query: formValue }),
headers: {
"Content-Type": "application/json",
},
method: "POST",
}
);

if (!res.ok) {
setIsError(true);
return;
}

const data = await res.json();

// Map the response data to facet filters.
const filters = mapResponse(data);

// Dispatch the update entity filters action.
exploreDispatch({
payload: { filters },
type: ExploreActionKind.UpdateEntityFilters,
});

// Reset the form.
setIsDirty(false);
(e.target as HTMLFormElement).reset();
} catch (err) {
console.error(err);
setIsError(true);
} finally {
setIsSubmitting(false);
}
},
[exploreDispatch]
);

return (
<StyledForm onSubmit={onSubmit}>
<StyledOutlinedInput
{...OUTLINED_INPUT_PROPS}
endAdornment={
<EndAdornment
adornmentType={getEndAdornmentType(isDirty, isSubmitting)}
/>
}
error={isError}
onChange={(): void => setIsDirty(true)}
/>
</StyledForm>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Box } from "@mui/material";
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import React from "react";
import { FacetAssistant } from "../facetAssistant";

const meta: Meta<typeof FacetAssistant> = {
component: FacetAssistant,
decorators: [
(Story): JSX.Element => (
<Box sx={{ width: 264 }}>
<Story />
</Box>
),
],
};

export default meta;

type Story = StoryObj<typeof meta>;

export const DefaultStory: Story = {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export interface AiResponse {
facets: Facet[];
}

export interface Facet {
facet: string;
selectedValues: SelectedValue[];
}

export interface SelectedValue {
mention: string;
term: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { SelectedFilter } from "../../../../../../common/entities";
import { AiResponse, Facet, SelectedValue } from "./types";

/**
* Filters out unknown facets.
* @param facet - Facet.
* @returns Boolean.
*/
function filterFacet(facet: Facet): boolean {
return facet.facet !== "unknown" && facet.facet !== "unmatched";
}

/**
* Filters out unknown values.
* @param value - Value.
* @returns Boolean.
*/
function filterValue(value: string): boolean {
return value !== "unknown";
}

/**
* Maps the AI response to a list of selected filters.
* @param response - AI response.
* @returns List of selected filters.
*/
export function mapResponse(response: AiResponse): SelectedFilter[] {
const { facets } = response;

// Filter out unknown facets.
const filteredFacets = facets.filter(filterFacet);

// Map facets to selected filters.
return filteredFacets
.map(mapSelectedFilter)
.filter((filter) => filter.value.length > 0);
}

/**
* Maps the facet to a selected filter.
* @param facet - Facet.
* @returns Selected filter.
*/
function mapSelectedFilter(facet: Facet): SelectedFilter {
const { facet: categoryKey, selectedValues } = facet;
return {
categoryKey,
value: selectedValues.map(mapValue).filter(filterValue),
};
}

/**
* Maps the selected value to a string.
* @param selectedValue - Selected value.
* @returns String.
*/
function mapValue(selectedValue: SelectedValue): string {
return selectedValue.term;
}
17 changes: 17 additions & 0 deletions src/components/common/CustomIcon/components/AiIcon/aiIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { SvgIcon, SvgIconProps } from "@mui/material";
import React from "react";

export const AiIcon = ({
fontSize = "xsmall",
viewBox = "0 0 18 18",
...props
}: SvgIconProps): JSX.Element => {
return (
<SvgIcon fontSize={fontSize} viewBox={viewBox} {...props}>
<path
d="M11.1162 6.90234C11.2961 6.9024 11.4745 6.95105 11.6299 7.0459C11.7834 7.15171 11.9008 7.30216 11.9668 7.47656L12.4541 8.94238C12.5992 9.39219 12.8493 9.80169 13.1836 10.1357C13.5179 10.4698 13.9271 10.7196 14.377 10.8643L15.8086 11.3711C15.9838 11.4261 16.1357 11.5388 16.2393 11.6904C16.3538 11.8377 16.4141 12.0176 16.4141 12.2041C16.4201 12.3885 16.3638 12.5695 16.2539 12.7178C16.1489 12.8712 15.9952 12.9849 15.8174 13.0391L14.376 13.5176C13.9309 13.6649 13.5259 13.9143 13.1953 14.2432C12.8635 14.5732 12.6151 14.9777 12.4707 15.4229L11.9639 16.8564C11.9131 17.0291 11.8064 17.18 11.6611 17.2861C11.5075 17.3914 11.3249 17.4481 11.1387 17.4473C10.9518 17.4552 10.7677 17.4003 10.6152 17.292C10.4626 17.1835 10.3499 17.0269 10.2959 16.8477L9.81543 15.4082C9.67122 14.9623 9.42202 14.5571 9.08984 14.2266C8.75607 13.8994 8.35384 13.6486 7.91211 13.4932L6.4707 13.0225C6.30005 12.9623 6.1501 12.8536 6.04004 12.71C5.92966 12.5587 5.86835 12.3767 5.86426 12.1895C5.8602 12.0023 5.91297 11.818 6.0166 11.6621C6.12376 11.5087 6.28117 11.3967 6.46094 11.3447L7.91016 10.8643C8.36039 10.7198 8.76986 10.4698 9.10449 10.1357C9.43912 9.80168 9.68974 9.39237 9.83496 8.94238L10.3145 7.51953C10.3633 7.34377 10.4677 7.18839 10.6123 7.07715C10.7569 6.96592 10.9338 6.90453 11.1162 6.90234ZM3.95801 2.82129C4.12861 2.81526 4.29719 2.86268 4.43945 2.95703C4.58008 3.05192 4.68784 3.18609 4.75 3.34473L5.10547 4.42285C5.20929 4.72108 5.37924 4.99253 5.60254 5.21582C5.82576 5.43896 6.09645 5.60911 6.39453 5.71289L7.44824 6.07617C7.60554 6.13033 7.74148 6.23319 7.83594 6.37012C7.93346 6.50778 7.98686 6.67407 7.9873 6.84277C7.9873 7.00805 7.93636 7.17082 7.84473 7.30664C7.7462 7.44318 7.60737 7.54552 7.44824 7.60059L6.38477 7.95605C6.08795 8.05868 5.81737 8.22579 5.59277 8.44531C5.37385 8.67009 5.20759 8.94062 5.10547 9.2373L4.74219 10.2832C4.69127 10.4445 4.58709 10.5836 4.44727 10.6787C4.30958 10.7762 4.14528 10.8287 3.97656 10.8291C3.80348 10.8284 3.63463 10.7733 3.49512 10.6709C3.36155 10.5712 3.26187 10.4325 3.20898 10.2744L2.86328 9.2207C2.7618 8.92528 2.59446 8.65685 2.37402 8.43555C2.15786 8.21005 1.8886 8.04258 1.59082 7.94824L0.536133 7.59277C0.373071 7.54121 0.231772 7.43734 0.132812 7.29785C0.0654437 7.19277 0.0217393 7.074 0.00585938 6.9502C-0.00996064 6.82648 0.00237279 6.70062 0.0410156 6.58203C0.0796627 6.46357 0.143911 6.35521 0.229492 6.26465C0.315194 6.17404 0.419905 6.10284 0.536133 6.05762L1.59082 5.71387C1.88888 5.60991 2.15969 5.4401 2.38281 5.2168C2.60612 4.99321 2.77627 4.72139 2.87988 4.42285L3.22559 3.38574C3.27404 3.22976 3.36761 3.09134 3.49512 2.98926C3.6284 2.88694 3.79014 2.82858 3.95801 2.82129ZM10.5156 0C10.6744 0.00096508 10.8295 0.0481569 10.9619 0.135742C11.0814 0.223688 11.1752 0.342585 11.2324 0.479492L11.4092 1.01953C11.4462 1.12253 11.503 1.21738 11.5771 1.29785C11.6552 1.37117 11.7475 1.42785 11.8477 1.46582L12.3789 1.64258C12.5138 1.69519 12.6326 1.78307 12.7227 1.89648C12.8148 2.0212 12.8657 2.17211 12.8672 2.32715C12.8604 2.48067 12.8099 2.62938 12.7227 2.75586C12.6359 2.87601 12.5125 2.9664 12.3711 3.01074L11.8477 3.1875C11.7459 3.22224 11.6532 3.28042 11.5771 3.35645C11.5013 3.43235 11.4439 3.52446 11.4092 3.62598L11.2236 4.1582C11.1708 4.2862 11.0866 4.39887 10.9785 4.48535C10.8544 4.58537 10.6994 4.63934 10.54 4.6377C10.3947 4.6291 10.2533 4.58578 10.1279 4.51172C10.0019 4.41845 9.90815 4.28692 9.85742 4.13965L9.68066 3.61816C9.64957 3.51476 9.59193 3.42101 9.5127 3.34766C9.43748 3.26809 9.34767 3.21243 9.24414 3.17969L8.7207 3.01074C8.58124 2.95739 8.45889 2.86686 8.36719 2.74902C8.27882 2.62302 8.23145 2.47218 8.23145 2.31836C8.23828 2.1649 8.28774 2.01608 8.375 1.88965C8.46456 1.77156 8.58796 1.68372 8.72852 1.63672L9.24219 1.46582C9.34512 1.42931 9.43985 1.37247 9.52051 1.29883C9.59458 1.21843 9.65249 1.12437 9.68945 1.02148L9.86621 0.49707C9.91523 0.366571 9.99619 0.250293 10.1025 0.160156C10.2227 0.0698452 10.3661 0.0145692 10.5156 0Z"
fill="currentColor"
/>
</SvgIcon>
);
};
4 changes: 2 additions & 2 deletions src/providers/exploreState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,7 @@ function exploreReducer(
*/
case ExploreActionKind.UpdateEntityFilters: {
const {
entityListType,
entityListType = state.tabValue,
filters: filterState,
grouping = [],
sorting = [],
Expand Down Expand Up @@ -662,7 +662,7 @@ function exploreReducer(
categoryGroupConfigKey
),
rowPreview: closeRowPreview(state.rowPreview),
...(payload.entityListType === state.tabValue
...(entityListType === state.tabValue
? {
filterCount: getFilterCount(filterState),
filterState,
Expand Down
2 changes: 1 addition & 1 deletion src/providers/exploreState/payloads/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export type ResetExploreResponsePayload = undefined;
* Update entity filters payload.
*/
export interface UpdateEntityFiltersPayload {
entityListType: string;
entityListType?: string;
filters: SelectedFilter[];
grouping?: GroupingState;
sorting?: ColumnSort[];
Expand Down
20 changes: 20 additions & 0 deletions src/utils/form.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { FormEvent } from "react";

/**
* Function to get the form value.
* @param e - Form event.
* @param fieldName - Field name.
* @returns Form value.
*/
export function getFormValue(
e: FormEvent<HTMLFormElement>,
fieldName: string
): string | undefined {
const formData = new FormData(e.target as HTMLFormElement);
const formValue = formData.get(fieldName);
// Expect the form value to be a string.
if (!formValue || typeof formValue !== "string") return;
// Expect the form value to not be empty.
if (formValue.trim() === "") return;
return formValue;
}
Loading
Loading