Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"Refactor highlight components and add support for multiple observations in transcript" #380

Merged
merged 7 commits into from
May 28, 2024
4 changes: 1 addition & 3 deletions src/stories/highlight/_types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ export interface WordProps extends ISpanProps {
currentTime?: number;
observations?: Observation[];
text: string;
/** Adjusts the font size. By default font size is medium */
size?: "xs" | "sm" | "md" | "lg" | "xl" | "xxl" | "xxxl";

tooltipContent?: (observation: Observation) => ReactNode;
tooltipContent?: (observations: Observation[]) => ReactNode;
}
43 changes: 37 additions & 6 deletions src/stories/highlight/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,31 @@ import { TDiarization } from "./demo-parts/transcript-diarization";
import { TParagraph } from "./demo-parts/transcript-paragraph";
import { Tag } from "../tags";
import { TSentiment } from "./demo-parts/transcript-sentiment";
import { styled } from "styled-components";

const StyledTag = styled(Tag)`
user-select: none;
position: relative;

&:before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: white;
z-index: -1;
}
`;

const TagsWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: ${({ theme }) => theme.space.xs};
`;

export interface StoryArgs extends HighlightArgs {
words: Array<WordProps & { speaker: number }>;
Expand All @@ -38,14 +63,16 @@ const Template: StoryFn<StoryArgs> = (args) => {
const handleAddObservation = () => {
if (selection) {
console.log("🚀 ~ handleAddObservation ~ selection:", selection);
const hue = '#' + (Math.random() * 0xFFFFFF << 0).toString(16).padStart(6, '0');
setObservations([
...observations,
{
id: observations.length,
start: selection.from,
end: selection.to,
label: `new observation (#${observations.length})`,
hue: '#'+(Math.random() * 0xFFFFFF << 0).toString(16).padStart(6, '0')
hue: getColor(hue, 700, undefined, 0.5),
color: hue,
},
]);
}
Expand Down Expand Up @@ -567,11 +594,15 @@ WithTooltip.args = {
...defaultArgs,
words: defaultArgs.words.map((w) => ({
...w,
tooltipContent: (obs: Observation) => (
<Tag hue={"red"} color="white" onClick={() => alert("Hey")}>
This is a tag of obs "{obs.label}"
</Tag>
),
tooltipContent: (obs: Observation[]) => (
<TagsWrapper>
{obs.map((o) => (
<StyledTag hue={o.hue} color={o.color} onClick={() => alert(o.label)}>
This is a tag of obs "{o.label}"
</StyledTag>
))}
</TagsWrapper>
)
})),
};

Expand Down
98 changes: 44 additions & 54 deletions src/stories/highlight/index.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,60 @@
import { Span as ZendeskSpan } from "@zendeskgarden/react-typography";
import { PropsWithChildren, useCallback, useEffect, useRef } from "react";
import { PropsWithChildren, useCallback, useEffect, useMemo, useRef } from "react";
import styled from "styled-components";
import { getColor } from "../theme/utils";
import { HighlightArgs, Observation, WordProps } from "./_types";
import { HighlightContextProvider } from "./highlightContext";
import { Searchable } from "./searchable";
import { Tooltip } from "../tooltip";
import { theme } from "../theme";

const StyledWord = styled(ZendeskSpan)<
WordProps & { observation?: Observation }
const StyledWord = styled.div<
WordProps & { observations?: Observation[] }
>`
display: inline;
font-size: ${({ theme, size }) => theme.fontSizes[size ?? "md"]};
padding: ${({ theme }) => theme.space.xxs} 0;
position: relative;

${({ observation, theme }) =>
observation &&
${({ observations, theme }) =>
observations && observations.length > 0 &&
`
background-color: ${
observation.hue ?? getColor(theme.palette.azure, 700, undefined, 0.5)
};
color: ${observation.color ?? "white"};
color: ${observations[observations.length - 1].color ?? theme.palette.grey[600]};
box-sizing: border-box;
font-weight: ${theme.fontWeights.semibold};

&:focus {
outline: none;
}

+ span:not([observation]) {
margin-left: 2px;
}
`}
`;

const ActiveWord = styled.span`
background-color: ${({ theme }) =>
getColor(theme.palette.fuschia, 400, undefined, 0.4)};
padding: 0 2px;
`;

const WordsContainer = styled.div`
box-sizing: border-box;
${StyledWord}, span {
&::selection {
background-color: ${({ theme }) =>
getColor(theme.palette.kale, 700, undefined, 0.5)};
getColor(theme.palette.kale, 700, undefined, 0.5)};
}
}
`;

const Layer = styled.div<{
color: string;
}>`
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
background-color: ${({ color }) => getColor(color, undefined, undefined, 0.2)};
`;

/**
* Use Highlight to use highlight interation on any text element
*/
Expand All @@ -57,6 +63,7 @@ const Highlight = (props: PropsWithChildren<HighlightArgs>) => {
const ref = useRef<HTMLDivElement>(null);

const extractText = (selection: Selection) => {
if(selection.anchorNode === null || selection.focusNode === null) return "";
var range = selection.getRangeAt(0);

var tempDiv = document.createElement("div");
Expand Down Expand Up @@ -131,49 +138,22 @@ const Word = (props: WordProps) => {
props.currentTime < props.end;

// Are there any observations containing this word?
const foundObservations = props.observations?.map((obs) =>
props.start >= obs.start && props.end <= obs.end ? obs : null
);

// Get the closer observation to the word
const observation = foundObservations?.reduce((prev, current) => {
if (!prev) return current;
if (!current) return prev;
return current.end - current.start < prev.end - prev.start ? current : prev;
}, null);

if (props.tooltipContent !== undefined && !!observation) {
return (
<Tooltip content={props.tooltipContent(observation)} isTransparent>
<StyledWord
{...props}
observation={observation}
data-start={props.start}
data-end={props.end}
className={!!observation ? "highlighted" : ""}
{...(!!observation ? { tag: "observation" } : {})}
>
{isActive ? (
<ActiveWord>
<Searchable text={props.text} />
</ActiveWord>
) : (
<Searchable text={props.text} />
)}{" "}
</StyledWord>
</Tooltip>
);
}
const foundObservations = useMemo(() =>
props.observations?.filter((obs) =>
props.start >= obs.start && props.end <= obs.end
) ?? [], [props.observations, props.start, props.end]);

return (
const ObsWord = useMemo(() => (
<StyledWord
{...props}
data-start={props.start}
data-end={props.end}
className={!!observation ? "highlighted" : ""}
{...(observation && { observation })}
{...(!!observation ? { tag: "observation" } : {})}
className={foundObservations.length > 0 ? "highlighted" : ""}
{...(foundObservations && { observations: foundObservations })}
>
{foundObservations.length > 0 && foundObservations.map((obs) => (
<Layer key={obs.id} color={obs.hue ?? theme.palette.grey[600]} />
))}
{isActive ? (
<ActiveWord>
<Searchable text={props.text} />
Expand All @@ -182,7 +162,17 @@ const Word = (props: WordProps) => {
<Searchable text={props.text} />
)}{" "}
</StyledWord>
);
), [props, foundObservations, isActive]);

if (props.tooltipContent !== undefined && foundObservations.length > 0) {
return (
<Tooltip content={props.tooltipContent(foundObservations)} isTransparent>
{ObsWord}
</Tooltip>
);
}

return <>{ObsWord}</>;
};

Highlight.Word = Word;
Expand Down
Loading