Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Stack, Typography } from "@mui/material";
import React from "react";
import { STACK_PROPS } from "../../../../../../../../../../../../styles/common/mui/stack";
import { TYPOGRAPHY_PROPS } from "../../../../../../../../../../../../styles/common/mui/typography";
import { ResultSummarySectionProps } from "./types";

export const ResultSummarySection = ({
icon,
mentionTermPair,
title,
}: ResultSummarySectionProps): JSX.Element | null => {
if (mentionTermPair.length === 0) return null;
return (
<Stack gap={2} useFlexGap>
<Typography variant={TYPOGRAPHY_PROPS.VARIANT.BODY_500}>
{title}
</Typography>
{mentionTermPair.map(([mention, term]) => (
<Stack
key={`${mention}-${term}`}
direction={STACK_PROPS.DIRECTION.ROW}
spacing={1}
useFlexGap
>
{icon}
<span>{mention}</span>
<Typography color={TYPOGRAPHY_PROPS.COLOR.INK_LIGHT}>=</Typography>
<span>{term}</span>
</Stack>
))}
</Stack>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ReactNode } from "react";
import { MentionTermPair } from "../../../../types";

export interface ResultSummarySectionProps {
icon: ReactNode;
mentionTermPair: MentionTermPair[];
title: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { TooltipProps } from "@mui/material";
import { FONT } from "../../../../../../../../../../styles/common/constants/font";
import { PALETTE } from "../../../../../../../../../../styles/common/constants/palette";

export const TOOLTIP_PROPS: Omit<TooltipProps, "children" | "title"> = {
disableInteractive: true,
enterNextDelay: 250,
placement: "top-start",
slotProps: {
popper: {
modifiers: [
{ name: "flip", options: { padding: 8 } },
{ name: "offset", options: { offset: [0, 2] } },
{ name: "preventOverflow", options: { padding: 8 } },
],
},
tooltip: {
sx: {
backgroundColor: PALETTE.COMMON_WHITE,
border: `1px solid ${PALETTE.SMOKE_DARK}`,
color: PALETTE.INK_MAIN,
font: FONT.BODY_400,
margin: 0,
maxWidth: "unset",
padding: 4,
},
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useCallback, useState } from "react";

/**
* Hook to measure the width of an element and provide a ref to attach to it.
* Returns the measured width and a ref function to attach to the element.
* @returns A tuple containing the measured width and a ref function to attach to the element.
*/
export function useMeasuredWidth<T extends HTMLElement>(): [
number | undefined,
(node: T | null) => void
] {
const [width, setWidth] = useState<number>();

const ref = useCallback((node: T | null) => {
if (!node) return;

const { width } = node.getBoundingClientRect();

setWidth(width);
}, []);

return [width, ref];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Tooltip as MTooltip } from "@mui/material";
import React from "react";
import { TOOLTIP_PROPS } from "./constants";
import { useMeasuredWidth } from "./hook";
import { TooltipProps } from "./types";

export const Tooltip = ({ children, ...props }: TooltipProps): JSX.Element => {
const [minWidth = "unset", ref] = useMeasuredWidth<HTMLSpanElement>();
return (
<MTooltip
{...TOOLTIP_PROPS}
slotProps={{
...TOOLTIP_PROPS.slotProps,
popper: { sx: { minWidth } },
}}
{...props}
>
{children(ref)}
</MTooltip>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { TooltipProps as MTooltipProps } from "@mui/material";
import { ReactElement } from "react";

export interface TooltipProps extends Omit<MTooltipProps, "children"> {
children: (ref: (node: HTMLSpanElement | null) => void) => ReactElement;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import styled from "@emotion/styled";
import { Typography } from "@mui/material";
import { PALETTE } from "../../../../../../../../styles/common/constants/palette";

export const StyledResultSummary = styled(Typography)`
cursor: pointer;
text-decoration: underline dashed ${PALETTE.SMOKE_DARK};
text-decoration-skip-ink: none;
text-decoration-thickness: 1px;
text-underline-position: under;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Stack } from "@mui/material";
import React from "react";
import { SVG_ICON_PROPS } from "../../../../../../../../styles/common/mui/svgIcon";
import { TYPOGRAPHY_PROPS } from "../../../../../../../../styles/common/mui/typography";
import { ErrorIcon } from "../../../../../../../common/CustomIcon/components/ErrorIcon/errorIcon";
import { SuccessIcon } from "../../../../../../../common/CustomIcon/components/SuccessIcon/successIcon";
import { ResultSummarySection } from "./components/Tooltip/components/Title/title";
import { Tooltip } from "./components/Tooltip/tooltip";
import { StyledResultSummary } from "./resultSummary.styles";
import { ResultSummaryProps } from "./types";
import { renderSummary } from "./utils";

export const ResultSummary = ({
summary,
}: ResultSummaryProps): JSX.Element | null => {
if (!summary) return null;
return (
<Tooltip
title={
<Stack gap={4} useFlexGap>
<ResultSummarySection
icon={<SuccessIcon color={SVG_ICON_PROPS.COLOR.SUCCESS} />}
mentionTermPair={summary.matched}
title="Matches"
/>
<ResultSummarySection
icon={<ErrorIcon color={SVG_ICON_PROPS.COLOR.ERROR} />}
mentionTermPair={summary.unmatched}
title="No matches"
/>
</Stack>
}
>
{(ref) => (
<StyledResultSummary
ref={ref}
variant={TYPOGRAPHY_PROPS.VARIANT.BODY_400}
>
{renderSummary(summary)}
</StyledResultSummary>
)}
</Tooltip>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type MentionTermPair = [mention: string, term: string];

export type ResultSummaryData = {
matched: MentionTermPair[];
unmatched: MentionTermPair[];
};

export interface ResultSummaryProps {
summary?: ResultSummaryData;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { ResultSummaryData } from "./types";

/**
* Returns the appropriate noun ("match" or "matches") based on the count.
* @param count - The number of matches.
* @returns The singular or plural form of "match".
*/
function getMatchNoun(count: number): string {
return count === 1 ? "match" : "matches";
}

/**
* Renders a human-readable summary from the result summary data.
* @param summary - The result summary data.
* @returns A formatted string summarizing the matched and unmatched items.
*/
export function renderSummary(summary: ResultSummaryData): string {
const matched = summary.matched.length;
const unmatched = summary.unmatched.length;

if (!unmatched) {
// If there are no unmatched items, just report the matched count.
return `AI found ${matched} ${getMatchNoun(matched)}`;
}

if (!matched) {
// If there are no matched items, report only unmatched count.
return `AI found ${unmatched} with no match`;
}

// Otherwise, report both matched and unmatched counts.
return `AI found ${matched} ${getMatchNoun(
matched
)} and ${unmatched} with no match`;
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import styled from "@emotion/styled";
import { OutlinedInput } from "@mui/material";
import { OutlinedInput, Stack } from "@mui/material";
import { PALETTE } from "../../../../../../styles/common/constants/palette";

export const StyledForm = styled.form`
export const StyledStack = styled(Stack)`
gap: 8px;
grid-column: 1 / -1;
`;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { ExploreActionKind } from "../../../../../../providers/exploreState";
import { getFormValue } from "../../../../../../utils/form";
import { EndAdornment } from "./components/EndAdornment/endAdornment";
import { getEndAdornmentType } from "./components/EndAdornment/utils";
import { ResultSummary } from "./components/ResultSummary/resultSummary";
import { OUTLINED_INPUT_PROPS } from "./constants";
import { StyledForm, StyledOutlinedInput } from "./facetAssistant.styles";
import { mapResponse } from "./utils";
import { StyledOutlinedInput, StyledStack } from "./facetAssistant.styles";
import { AiResponse } from "./types";
import { buildSummary, mapResponse } from "./utils";

/**
* AI-powered facet assistant component.
Expand All @@ -18,6 +20,7 @@ export const FacetAssistant = (): JSX.Element => {
const [isDirty, setIsDirty] = useState<boolean>(false);
const [isError, setIsError] = useState<boolean>(false);
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [response, setResponse] = useState<AiResponse | null>(null);

const onSubmit = useCallback(
async (e: FormEvent<HTMLFormElement>): Promise<void> => {
Expand All @@ -27,8 +30,9 @@ export const FacetAssistant = (): JSX.Element => {
const formValue = getFormValue(e, "query-to-facets");
if (!formValue) return;

setIsSubmitting(true);
setIsError(false);
setIsSubmitting(true);
setResponse(null);

try {
const res = await fetch(
Expand All @@ -49,6 +53,9 @@ export const FacetAssistant = (): JSX.Element => {

const data = await res.json();

// Set the response data
setResponse(data);

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

Expand All @@ -63,6 +70,7 @@ export const FacetAssistant = (): JSX.Element => {
(e.target as HTMLFormElement).reset();
} catch (err) {
console.error(err);
setResponse(null);
setIsError(true);
} finally {
setIsSubmitting(false);
Expand All @@ -72,17 +80,20 @@ export const FacetAssistant = (): JSX.Element => {
);

return (
<StyledForm onSubmit={onSubmit}>
<StyledOutlinedInput
{...OUTLINED_INPUT_PROPS}
endAdornment={
<EndAdornment
adornmentType={getEndAdornmentType(isDirty, isSubmitting)}
/>
}
error={isError}
onChange={(): void => setIsDirty(true)}
/>
</StyledForm>
<StyledStack useFlexGap>
<form onSubmit={onSubmit}>
<StyledOutlinedInput
{...OUTLINED_INPUT_PROPS}
endAdornment={
<EndAdornment
adornmentType={getEndAdornmentType(isDirty, isSubmitting)}
/>
}
error={isError}
onChange={(): void => setIsDirty(true)}
/>
</form>
<ResultSummary summary={buildSummary(response)} />
</StyledStack>
);
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export interface AiResponse {
facets: Facet[];
query: string;
}

export interface Facet {
Expand All @@ -9,5 +10,6 @@ export interface Facet {

export interface SelectedValue {
mention: string;
recognized: boolean;
term: string;
}
Loading