mirror of
https://github.com/tencentmusic/supersonic.git
synced 2026-04-18 20:34:19 +08:00
add chat plugin and split query to parse and execute (#25)
* [feature](webapp) add drill down dimensions and metric period compare and modify layout * [feature](webapp) add drill down dimensions and metric period compare and modify layout * [feature](webapp) gitignore add supersonic-webapp * [feature](webapp) gitignore add supersonic-webapp * [feature](webapp) add chat plugin and split query to parse and execute * [feature](webapp) add chat plugin and split query to parse and execute * [feature](webapp) add chat plugin and split query to parse and execute --------- Co-authored-by: williamhliu <williamhliu@tencent.com>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import { PREFIX_CLS } from '../../common/constants';
|
||||
import { MsgDataType } from '../../common/type';
|
||||
import ChatMsg from '../ChatMsg';
|
||||
import Tools from '../Tools';
|
||||
import Text from './Text';
|
||||
import Typing from './Typing';
|
||||
|
||||
type Props = {
|
||||
question: string;
|
||||
executeLoading: boolean;
|
||||
chartIndex: number;
|
||||
executeTip?: string;
|
||||
data?: MsgDataType;
|
||||
isMobileMode?: boolean;
|
||||
triggerResize?: boolean;
|
||||
isLastMessage?: boolean;
|
||||
onSwitchEntity: (entityId: string) => void;
|
||||
onChangeChart: () => void;
|
||||
};
|
||||
|
||||
const ExecuteItem: React.FC<Props> = ({
|
||||
question,
|
||||
executeLoading,
|
||||
chartIndex,
|
||||
executeTip,
|
||||
data,
|
||||
isMobileMode,
|
||||
triggerResize,
|
||||
isLastMessage,
|
||||
onSwitchEntity,
|
||||
onChangeChart,
|
||||
}) => {
|
||||
const prefixCls = `${PREFIX_CLS}-item`;
|
||||
|
||||
if (executeLoading) {
|
||||
return <Typing />;
|
||||
}
|
||||
|
||||
if (executeTip) {
|
||||
return <Text data={executeTip} />;
|
||||
}
|
||||
|
||||
if (!data || data.queryMode === 'WEB_PAGE') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isMetricCard =
|
||||
(data.queryMode === 'METRIC_DOMAIN' || data.queryMode === 'METRIC_FILTER') &&
|
||||
data.queryResults?.length === 1;
|
||||
|
||||
return (
|
||||
<div className={`${prefixCls}-msg-content`}>
|
||||
<ChatMsg
|
||||
question={question}
|
||||
data={data}
|
||||
chartIndex={chartIndex}
|
||||
isMobileMode={isMobileMode}
|
||||
triggerResize={triggerResize}
|
||||
/>
|
||||
{!isMetricCard && (
|
||||
<Tools
|
||||
data={data}
|
||||
isLastMessage={isLastMessage}
|
||||
isMobileMode={isMobileMode}
|
||||
onSwitchEntity={onSwitchEntity}
|
||||
onChangeChart={onChangeChart}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExecuteItem;
|
||||
205
webapp/packages/chat-sdk/src/components/ChatItem/ParseTip.tsx
Normal file
205
webapp/packages/chat-sdk/src/components/ChatItem/ParseTip.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { AGG_TYPE_MAP, PREFIX_CLS } from '../../common/constants';
|
||||
import { ChatContextType } from '../../common/type';
|
||||
import Text from './Text';
|
||||
import Typing from './Typing';
|
||||
import classNames from 'classnames';
|
||||
|
||||
type Props = {
|
||||
parseLoading: boolean;
|
||||
parseInfoOptions: ChatContextType[];
|
||||
parseTip: string;
|
||||
currentParseInfo?: ChatContextType;
|
||||
onSelectParseInfo: (parseInfo: ChatContextType) => void;
|
||||
};
|
||||
|
||||
const MAX_OPTION_VALUES_COUNT = 2;
|
||||
|
||||
const ParseTip: React.FC<Props> = ({
|
||||
parseLoading,
|
||||
parseInfoOptions,
|
||||
parseTip,
|
||||
currentParseInfo,
|
||||
onSelectParseInfo,
|
||||
}) => {
|
||||
const prefixCls = `${PREFIX_CLS}-item`;
|
||||
|
||||
if (parseLoading) {
|
||||
return <Typing />;
|
||||
}
|
||||
|
||||
if (parseTip) {
|
||||
return <Text data={parseTip} />;
|
||||
}
|
||||
|
||||
if (parseInfoOptions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const getTipNode = (parseInfo: ChatContextType, isOptions?: boolean, index?: number) => {
|
||||
const {
|
||||
domainName,
|
||||
dateInfo,
|
||||
dimensionFilters,
|
||||
dimensions,
|
||||
metrics,
|
||||
aggType,
|
||||
queryMode,
|
||||
properties,
|
||||
entity,
|
||||
elementMatches,
|
||||
} = parseInfo || {};
|
||||
const { startDate, endDate } = dateInfo || {};
|
||||
const dimensionItems = dimensions?.filter(item => item.type === 'DIMENSION');
|
||||
const metric = metrics?.[0];
|
||||
|
||||
const tipContentClass = classNames(`${prefixCls}-tip-content`, {
|
||||
[`${prefixCls}-tip-content-option`]: isOptions,
|
||||
[`${prefixCls}-tip-content-option-active`]:
|
||||
isOptions &&
|
||||
currentParseInfo &&
|
||||
JSON.stringify(currentParseInfo) === JSON.stringify(parseInfo),
|
||||
[`${prefixCls}-tip-content-option-disabled`]:
|
||||
isOptions &&
|
||||
currentParseInfo !== undefined &&
|
||||
JSON.stringify(currentParseInfo) !== JSON.stringify(parseInfo),
|
||||
});
|
||||
|
||||
const itemValueClass = classNames({
|
||||
[`${prefixCls}-tip-item-value`]: !isOptions,
|
||||
[`${prefixCls}-tip-item-option`]: isOptions,
|
||||
});
|
||||
|
||||
const entityAlias = entity?.alias?.[0]?.split('.')?.[0];
|
||||
const entityName = elementMatches?.find(item => item.element?.type === 'ID')?.element.name;
|
||||
|
||||
const pluginName = properties?.CONTEXT?.plugin?.name;
|
||||
|
||||
const modeName = pluginName
|
||||
? '调插件'
|
||||
: queryMode.includes('METRIC')
|
||||
? '算指标'
|
||||
: queryMode === 'ENTITY_DETAIL'
|
||||
? '查明细'
|
||||
: queryMode === 'ENTITY_LIST_FILTER'
|
||||
? '做圈选'
|
||||
: '';
|
||||
|
||||
const fields =
|
||||
queryMode === 'ENTITY_DETAIL' ? dimensionItems?.concat(metrics || []) : dimensionItems;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={tipContentClass}
|
||||
onClick={() => {
|
||||
if (isOptions && currentParseInfo === undefined) {
|
||||
onSelectParseInfo(parseInfo);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{index !== undefined && <div>{index + 1}.</div>}
|
||||
{!pluginName && isOptions && <div className={`${prefixCls}-mode-name`}>{modeName}:</div>}
|
||||
{!!pluginName ? (
|
||||
<div className={`${prefixCls}-tip-item`}>
|
||||
将由问答插件
|
||||
<span className={itemValueClass}>{pluginName}</span>来解答
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{queryMode === 'METRIC_ENTITY' || queryMode === 'ENTITY_DETAIL' ? (
|
||||
<div className={`${prefixCls}-tip-item`}>
|
||||
<div className={`${prefixCls}-tip-item-name`}>{entityAlias}:</div>
|
||||
<div className={itemValueClass}>{entityName}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`${prefixCls}-tip-item`}>
|
||||
<div className={`${prefixCls}-tip-item-name`}>主题域:</div>
|
||||
<div className={itemValueClass}>{domainName}</div>
|
||||
</div>
|
||||
)}
|
||||
{modeName === '算指标' && metric && (
|
||||
<div className={`${prefixCls}-tip-item`}>
|
||||
<div className={`${prefixCls}-tip-item-name`}>指标:</div>
|
||||
<div className={itemValueClass}>{metric.name}</div>
|
||||
</div>
|
||||
)}
|
||||
{!isOptions && (
|
||||
<div className={`${prefixCls}-tip-item`}>
|
||||
<div className={`${prefixCls}-tip-item-name`}>时间:</div>
|
||||
<div className={itemValueClass}>
|
||||
{startDate === endDate ? startDate : `${startDate} ~ ${endDate}`}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{['METRIC_GROUPBY', 'METRIC_ORDERBY', 'ENTITY_DETAIL'].includes(queryMode) &&
|
||||
fields &&
|
||||
fields.length > 0 && (
|
||||
<div className={`${prefixCls}-tip-item`}>
|
||||
<div className={`${prefixCls}-tip-item-name`}>
|
||||
{queryMode === 'ENTITY_DETAIL' ? '查询字段' : '下钻维度'}:
|
||||
</div>
|
||||
<div className={itemValueClass}>
|
||||
{fields
|
||||
.slice(0, MAX_OPTION_VALUES_COUNT)
|
||||
.map(field => field.name)
|
||||
.join('、')}
|
||||
{fields.length > MAX_OPTION_VALUES_COUNT && '...'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{['METRIC_FILTER', 'METRIC_ENTITY', 'ENTITY_DETAIL', 'ENTITY_LIST_FILTER'].includes(
|
||||
queryMode
|
||||
) &&
|
||||
dimensionFilters &&
|
||||
dimensionFilters?.length > 0 && (
|
||||
<div className={`${prefixCls}-tip-item`}>
|
||||
<div className={`${prefixCls}-tip-item-name`}>筛选条件:</div>
|
||||
{dimensionFilters.slice(0, MAX_OPTION_VALUES_COUNT).map((filter, index) => (
|
||||
<div className={itemValueClass}>
|
||||
<span>{filter.name}:</span>
|
||||
<span>
|
||||
{Array.isArray(filter.value) ? filter.value.join('、') : filter.value}
|
||||
</span>
|
||||
{index !== dimensionFilters.length - 1 && <span>、</span>}
|
||||
</div>
|
||||
))}
|
||||
{dimensionFilters.length > MAX_OPTION_VALUES_COUNT && '...'}
|
||||
</div>
|
||||
)}
|
||||
{queryMode === 'METRIC_ORDERBY' && aggType && aggType !== 'NONE' && (
|
||||
<div className={`${prefixCls}-tip-item`}>
|
||||
<div className={`${prefixCls}-tip-item-name`}>聚合方式:</div>
|
||||
<div className={itemValueClass}>{AGG_TYPE_MAP[aggType]}</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
let tipNode: ReactNode;
|
||||
|
||||
if (parseInfoOptions.length > 1) {
|
||||
tipNode = (
|
||||
<div className={`${prefixCls}-multi-options`}>
|
||||
<div>您的问题解析为以下几项,请您点击确认</div>
|
||||
<div className={`${prefixCls}-options`}>
|
||||
{parseInfoOptions.map((item, index) => getTipNode(item, true, index))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const pluginName = parseInfoOptions[0]?.properties?.CONTEXT?.plugin?.name;
|
||||
tipNode = (
|
||||
<div className={`${prefixCls}-tip`}>
|
||||
<div>{!!pluginName ? '您的问题' : '您的问题解析为:'}</div>
|
||||
{getTipNode(parseInfoOptions[0])}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <Text data={tipNode} />;
|
||||
};
|
||||
|
||||
export default ParseTip;
|
||||
@@ -1,71 +1,112 @@
|
||||
import { MsgDataType } from '../../common/type';
|
||||
import { ChatContextType, MsgDataType, ParseStateEnum } from '../../common/type';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Typing from './Typing';
|
||||
import ChatMsg from '../ChatMsg';
|
||||
import { chatQuery } from '../../service';
|
||||
import { PARSE_ERROR_TIP, PREFIX_CLS } from '../../common/constants';
|
||||
import Text from './Text';
|
||||
import Tools from '../Tools';
|
||||
import { chatExecute, chatParse, switchEntity } from '../../service';
|
||||
import { PARSE_ERROR_TIP, PREFIX_CLS, SEARCH_EXCEPTION_TIP } from '../../common/constants';
|
||||
import IconFont from '../IconFont';
|
||||
import ParseTip from './ParseTip';
|
||||
import ExecuteItem from './ExecuteItem';
|
||||
|
||||
type Props = {
|
||||
msg: string;
|
||||
followQuestions?: string[];
|
||||
conversationId?: number;
|
||||
domainId?: number;
|
||||
filter?: any[];
|
||||
isLastMessage?: boolean;
|
||||
msgData?: MsgDataType;
|
||||
isMobileMode?: boolean;
|
||||
triggerResize?: boolean;
|
||||
onMsgDataLoaded?: (data: MsgDataType) => void;
|
||||
onMsgDataLoaded?: (data: MsgDataType, valid: boolean) => void;
|
||||
onUpdateMessageScroll?: () => void;
|
||||
};
|
||||
|
||||
const ChatItem: React.FC<Props> = ({
|
||||
msg,
|
||||
followQuestions,
|
||||
conversationId,
|
||||
domainId,
|
||||
filter,
|
||||
isLastMessage,
|
||||
isMobileMode,
|
||||
triggerResize,
|
||||
msgData,
|
||||
onMsgDataLoaded,
|
||||
onUpdateMessageScroll,
|
||||
}) => {
|
||||
const [data, setData] = useState<MsgDataType>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [tip, setTip] = useState('');
|
||||
const [parseLoading, setParseLoading] = useState(false);
|
||||
const [parseInfo, setParseInfo] = useState<ChatContextType>();
|
||||
const [parseInfoOptions, setParseInfoOptions] = useState<ChatContextType[]>([]);
|
||||
const [parseTip, setParseTip] = useState('');
|
||||
const [executeLoading, setExecuteLoading] = useState(false);
|
||||
const [executeTip, setExecuteTip] = useState('');
|
||||
const [executeMode, setExecuteMode] = useState(false);
|
||||
const [entitySwitching, setEntitySwitching] = useState(false);
|
||||
|
||||
const [chartIndex, setChartIndex] = useState(0);
|
||||
|
||||
const prefixCls = `${PREFIX_CLS}-item`;
|
||||
|
||||
const updateData = (res: Result<MsgDataType>) => {
|
||||
if (res.code === 401 || res.code === 412) {
|
||||
setTip(res.msg);
|
||||
setExecuteTip(res.msg);
|
||||
return false;
|
||||
}
|
||||
if (res.code !== 200) {
|
||||
setTip(PARSE_ERROR_TIP);
|
||||
setExecuteTip(SEARCH_EXCEPTION_TIP);
|
||||
return false;
|
||||
}
|
||||
const { queryColumns, queryResults, queryState, queryMode } = res.data || {};
|
||||
const { queryColumns, queryResults, queryState, queryMode, response } = res.data || {};
|
||||
if (queryState !== 'SUCCESS') {
|
||||
setTip(PARSE_ERROR_TIP);
|
||||
setExecuteTip(response && typeof response === 'string' ? response : SEARCH_EXCEPTION_TIP);
|
||||
return false;
|
||||
}
|
||||
if ((queryColumns && queryColumns.length > 0 && queryResults) || queryMode === 'INSTRUCTION') {
|
||||
if ((queryColumns && queryColumns.length > 0 && queryResults) || queryMode === 'WEB_PAGE') {
|
||||
setData(res.data);
|
||||
setTip('');
|
||||
setExecuteTip('');
|
||||
return true;
|
||||
}
|
||||
setTip(PARSE_ERROR_TIP);
|
||||
return false;
|
||||
setExecuteTip(SEARCH_EXCEPTION_TIP);
|
||||
return true;
|
||||
};
|
||||
|
||||
const onExecute = async (parseInfoValue: ChatContextType, isSwitch?: boolean) => {
|
||||
setExecuteMode(true);
|
||||
setExecuteLoading(true);
|
||||
const { data } = await chatExecute(msg, conversationId!, parseInfoValue);
|
||||
setExecuteLoading(false);
|
||||
const valid = updateData(data);
|
||||
if (onMsgDataLoaded && !isSwitch) {
|
||||
onMsgDataLoaded({ ...data.data, chatContext: parseInfoValue }, valid);
|
||||
}
|
||||
};
|
||||
|
||||
const onSendMsg = async () => {
|
||||
setLoading(true);
|
||||
const semanticRes = await chatQuery(msg, conversationId, domainId);
|
||||
updateData(semanticRes.data);
|
||||
if (onMsgDataLoaded) {
|
||||
onMsgDataLoaded(semanticRes.data.data);
|
||||
setParseLoading(true);
|
||||
const { data: parseData } = await chatParse(msg, conversationId, domainId, filter);
|
||||
setParseLoading(false);
|
||||
const { code, data } = parseData || {};
|
||||
const { state, selectedParses } = data || {};
|
||||
if (
|
||||
code !== 200 ||
|
||||
state === ParseStateEnum.FAILED ||
|
||||
selectedParses == null ||
|
||||
selectedParses.length === 0 ||
|
||||
(selectedParses.length === 1 &&
|
||||
!selectedParses[0]?.domainName &&
|
||||
!selectedParses[0]?.properties?.CONTEXT?.plugin?.name &&
|
||||
selectedParses[0]?.queryMode !== 'WEB_PAGE')
|
||||
) {
|
||||
setParseTip(PARSE_ERROR_TIP);
|
||||
return;
|
||||
}
|
||||
if (onUpdateMessageScroll) {
|
||||
onUpdateMessageScroll();
|
||||
}
|
||||
setParseInfoOptions(selectedParses || []);
|
||||
if (selectedParses.length === 1) {
|
||||
const parseInfoValue = selectedParses[0];
|
||||
setParseInfo(parseInfoValue);
|
||||
onExecute(parseInfoValue);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -73,55 +114,66 @@ const ChatItem: React.FC<Props> = ({
|
||||
return;
|
||||
}
|
||||
if (msgData) {
|
||||
setParseInfoOptions([msgData.chatContext]);
|
||||
setExecuteMode(true);
|
||||
updateData({ code: 200, data: msgData, msg: 'success' });
|
||||
} else if (msg) {
|
||||
onSendMsg();
|
||||
}
|
||||
}, [msg, msgData]);
|
||||
|
||||
const prefixCls = `${PREFIX_CLS}-item`;
|
||||
const onSwitchEntity = async (entityId: string) => {
|
||||
setEntitySwitching(true);
|
||||
const res = await switchEntity(entityId, data?.chatContext?.domainId, conversationId || 0);
|
||||
setEntitySwitching(false);
|
||||
setData(res.data.data);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={prefixCls}>
|
||||
<IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />
|
||||
<Typing />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const onChangeChart = () => {
|
||||
setChartIndex(chartIndex + 1);
|
||||
};
|
||||
|
||||
if (tip) {
|
||||
return (
|
||||
<div className={prefixCls}>
|
||||
<IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />
|
||||
<Text data={tip} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data || data.queryMode === 'INSTRUCTION') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isMetricCard =
|
||||
(data.queryMode === 'METRIC_DOMAIN' || data.queryMode === 'METRIC_FILTER') &&
|
||||
data.queryResults?.length === 1;
|
||||
const onSelectParseInfo = async (parseInfoValue: ChatContextType) => {
|
||||
setParseInfo(parseInfoValue);
|
||||
onExecute(parseInfoValue, parseInfo !== undefined);
|
||||
if (onUpdateMessageScroll) {
|
||||
onUpdateMessageScroll();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={prefixCls}>
|
||||
<IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />
|
||||
<div className={`${prefixCls}-content`}>
|
||||
<ChatMsg
|
||||
question={msg}
|
||||
followQuestions={followQuestions}
|
||||
data={data}
|
||||
isMobileMode={isMobileMode}
|
||||
triggerResize={triggerResize}
|
||||
/>
|
||||
{!isMetricCard && (
|
||||
<Tools data={data} isLastMessage={isLastMessage} isMobileMode={isMobileMode} />
|
||||
)}
|
||||
<div className={`${prefixCls}-section`}>
|
||||
<IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />
|
||||
<div className={`${prefixCls}-content`}>
|
||||
<ParseTip
|
||||
parseLoading={parseLoading}
|
||||
parseInfoOptions={parseInfoOptions}
|
||||
parseTip={parseTip}
|
||||
currentParseInfo={parseInfo}
|
||||
onSelectParseInfo={onSelectParseInfo}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{executeMode && data?.queryMode !== 'WEB_PAGE' && (
|
||||
<div className={`${prefixCls}-section`}>
|
||||
<IconFont type="icon-zhinengsuanfa" className={`${prefixCls}-avatar`} />
|
||||
<div className={`${prefixCls}-content`}>
|
||||
<ExecuteItem
|
||||
question={msg}
|
||||
executeLoading={executeLoading}
|
||||
executeTip={executeTip}
|
||||
chartIndex={chartIndex}
|
||||
data={data}
|
||||
isMobileMode={isMobileMode}
|
||||
isLastMessage={isLastMessage}
|
||||
triggerResize={triggerResize}
|
||||
onSwitchEntity={onSwitchEntity}
|
||||
onChangeChart={onChangeChart}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,101 @@
|
||||
|
||||
.@{chat-item-prefix-cls} {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 20px;
|
||||
width: 100%;
|
||||
|
||||
&-section {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&-content-text {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
&-msg-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&-multi-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 12px;
|
||||
padding: 4px 0 12px;
|
||||
}
|
||||
|
||||
&-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
&-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
row-gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
&-tip-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 6px;
|
||||
column-gap: 12px;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
&-tip-content-option {
|
||||
padding: 6px 14px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--border-color-base);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--chat-blue);
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
|
||||
&-tip-content-option-disabled {
|
||||
cursor: auto;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-color-secondary);
|
||||
border-color: var(--border-color-base);
|
||||
}
|
||||
}
|
||||
|
||||
&-tip-content-option-active {
|
||||
border-color: var(--chat-blue);
|
||||
color: var(--chat-blue);
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
&-tip-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-mode-name {
|
||||
margin-right: -10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&-tip-item-value {
|
||||
color: var(--chat-blue);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&-tip-item-option {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&-avatar {
|
||||
display: flex;
|
||||
@@ -41,7 +136,7 @@
|
||||
|
||||
&-typing-bubble {
|
||||
width: fit-content;
|
||||
padding: 16px !important;
|
||||
// padding: 16px !important;
|
||||
}
|
||||
|
||||
&-text-bubble {
|
||||
|
||||
Reference in New Issue
Block a user