* -------------------------------------------------------------------------
* This file is part of the MindStudio project.
* Copyright (c) 2025 Huawei Technologies Co.,Ltd.
*
* MindStudio is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
* -------------------------------------------------------------------------
*/
import type { Theme } from '@emotion/react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Tooltip } from '@insight/lib/components';
import { action } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as ThumbIcon } from '../assets/images/chartHeader/thumbIcon.svg';
import { useRootStore } from '../context/context';
import type { InsightTemplate } from '../entity/insight';
import type { Session } from '../entity/session';
import { platform } from '../platforms';
import { getAutoKey } from '../utils/dataAutoKey';
import { logger } from '../utils/Logger';
interface ThumbTipProps {
width: number;
left: number;
}
interface RecommendedTemplateData {
timestamp: number;
templates: string[];
};
const ICON_SIZE = 18;
const ThumbTipContainer = styled.div<ThumbTipProps>`
overflow: hidden;
width: ${ICON_SIZE}px;
height: ${ICON_SIZE}px;
border-radius: ${ICON_SIZE / 2}px;
position: absolute;
z-index: 3;
transition: width 0.2s linear;
background-color: rgba(255, 255, 255, 0.2);
left: calc(${(props): number => props.left}% - ${(ICON_SIZE / 2) + 3}px);
padding: 0;
margin: 0 3px;
width: ${(props): number => props.width}px;
&:hover {
z-index: 2;
cursor: pointer;
}
.thumbIcon {
background-color: ${(props): string => props.theme.thumbIconBackgroundColor}
}
.barIcons {
float: left;
display: flex;
align-items: center;
justify-content: center;
}
`;
const IconContainer = styled.div`
width: ${ICON_SIZE}px;
height: ${ICON_SIZE}px;
border-radius: 50%;
padding: 0;
margin: 0;
`;
const ThumbEntranceBar = styled.div`
width: 100%;
height: 100%;
position: absolute;
top: 7.5px;
`;
const EmptyIcon = (): JSX.Element => <div>?</div>;
const Icon = React.forwardRef((
{ children, ...props }: { children?: React.ReactElement } & React.HTMLAttributes<unknown>,
ref?: React.ForwardedRef<HTMLDivElement>,
): JSX.Element => {
return <IconContainer {...props} ref={ref}>
{React.cloneElement(children ?? <EmptyIcon />, { style: { width: 15, height: 15 } })}
</IconContainer>;
});
Icon.displayName = 'insight-icon';
const TipTitle = ({ tipsTexts, template, theme }: { tipsTexts: string[]; template: InsightTemplate; theme: Theme }): JSX.Element => {
return <span style={{ fontSize: 14, color: theme.fontColor }}>
{tipsTexts[0]}
<Icon style={{ display: 'inline-block', verticalAlign: 'text-bottom', margin: '0 2px' }}>{template.icon}</Icon>
{`${template.name} ${tipsTexts[1]}`}
</span>;
};
type TemplatesIconsProps = {
left: number;
recommendedTemplates: RecommendedTemplateData['templates'];
} & InitialValue;
const TemplatesIcons = ({ left, recommendedTemplates, createSession, templates, theme, tipsTexts }: TemplatesIconsProps): JSX.Element => {
const filterTemplates = templates.filter((template) => recommendedTemplates.includes(template.id));
const [entranceWidth, setEntranceWidth] = React.useState(ICON_SIZE);
const onFocusThumbEntrance = (): void => { setEntranceWidth(entranceWidth === ICON_SIZE ? ICON_SIZE * (filterTemplates.length + 1) : ICON_SIZE); };
return (<ThumbTipContainer
width={entranceWidth}
left={left}
onMouseEnter={onFocusThumbEntrance}
onMouseLeave={onFocusThumbEntrance}
>
<Icon className="thumbIcon barIcons"><ThumbIcon fill={theme.contentBackgroundColor} /></Icon>
{filterTemplates.map((template) => (
<Tooltip
key={getAutoKey(template)}
title={<TipTitle tipsTexts={tipsTexts} template={template} theme={theme} />}
overlayInnerStyle={{ borderRadius: 18, padding: '6px 19px', whiteSpace: 'nowrap', textAlign: 'center' }}
overlayStyle={{ maxWidth: 500, display: entranceWidth < 1.5 * ICON_SIZE ? 'none' : '' }}
color={theme.toolTipBackgroundColor}
align={{ offset: [0, -5] }}
autoAdjustOverflow
placement="bottom"
><Icon className="barIcons" onClick={(): void => createSession(template)}>{template.icon}</Icon></Tooltip>
))}
</ThumbTipContainer>);
};
const transformTimeToLeft = (domainStart: number, domainEnd: number, timestamp: number): number => {
if (domainEnd === domainStart) {
return 0;
}
return 100 * (timestamp - domainStart) / (domainEnd - domainStart);
};
type ThumbEntranceProps = {
domainStart: number;
domainEnd: number;
recommendedTemplates: RecommendedTemplateData[];
} & InitialValue;
const ThumbEntrance = ({ domainStart, domainEnd, recommendedTemplates, ...props }: ThumbEntranceProps): JSX.Element => {
return (
<ThumbEntranceBar>
{recommendedTemplates.map((recommendedTemplate, index) => {
if (recommendedTemplate.timestamp > domainStart && recommendedTemplate.timestamp < domainEnd) {
return <TemplatesIcons
key={index}
left={transformTimeToLeft(domainStart, domainEnd, recommendedTemplate.timestamp)}
recommendedTemplates={recommendedTemplate.templates}
{...props}
/>;
} else {
return null;
}
})}
</ThumbEntranceBar>
);
};
interface InitialValue {
createSession: (template: InsightTemplate) => void;
templates: InsightTemplate[];
theme: Theme;
tipsTexts: string[];
};
const useInitial = (): InitialValue => {
const theme = useTheme();
const { t: i18n } = useTranslation();
const tipsTexts = i18n('RecommendedTemplate').split('&');
const { insightStore, sessionStore } = useRootStore();
return {
createSession: action(async (template: InsightTemplate) => {
platform.trace('openRecommend', { template: template.name });
const sessionConf: Partial<Session> = {
name: template.name,
units: insightStore.get(template.id)?.units?.map(Unit => new Unit(null as never)) ?? [],
availableUnits: insightStore.get(template.id)?.availableUnits?.map(Unit => new Unit(null as never)) ?? [],
icon: template.icon,
isNsMode: template.isNsMode,
};
sessionStore.activeSession = await sessionStore.newSession(sessionConf) ?? {} as Session;
}),
templates: Array.from(insightStore.templates.values()),
theme,
tipsTexts,
};
};
const Recommendations = observer(({ session }: { session: Session }): JSX.Element => {
* initialize value, includes:
* [createSession] -> the function to create a new session,
* [templates] -> all supportive template array,
* [theme] -> current DevEco theme,
* [tipsTexts] -> Chinese/English texts resourcce from i18n.
**/
const initialValue = useInitial();
const { domainRange: { domainStart, domainEnd }, endTimeAll } = session;
const fetch = async (_session: Session): Promise<RecommendedTemplateData[]> => [];
const [recommandedTemplateData, setRecommandedTemplateData] = React.useState<RecommendedTemplateData[]>([]);
const updateData = async (): Promise<void> => {
setRecommandedTemplateData(await fetch(session).catch(() => {
logger('Recommendations', 'fetch data error', 'warn');
return [];
}));
};
React.useEffect(() => {
updateData();
}, [domainStart, domainEnd, endTimeAll]);
return <ThumbEntrance {...session.domainRange} {...initialValue} recommendedTemplates={recommandedTemplateData} />;
});
export default Recommendations;