diff --git a/webapp/packages/chat-sdk/package.json b/webapp/packages/chat-sdk/package.json index 732194368..fbfac40fc 100644 --- a/webapp/packages/chat-sdk/package.json +++ b/webapp/packages/chat-sdk/package.json @@ -1,11 +1,12 @@ { "name": "supersonic-chat-sdk", - "version": "0.0.0", + "version": "0.3.0", "main": "dist/index.es.js", "module": "dist/index.es.js", "unpkg": "dist/index.umd.js", "types": "dist/index.d.ts", "dependencies": { + "@uiw/react-watermark": "^0.0.5", "antd": "^5.5.2", "axios": "^1.4.0", "classnames": "^2.3.2", @@ -191,4 +192,4 @@ "engines": { "node": ">=14.18.0" } -} +} \ No newline at end of file diff --git a/webapp/packages/chat-sdk/rollup/rollup.esm.config.mjs b/webapp/packages/chat-sdk/rollup/rollup.esm.config.mjs index 7c4fb53fd..0beef788a 100644 --- a/webapp/packages/chat-sdk/rollup/rollup.esm.config.mjs +++ b/webapp/packages/chat-sdk/rollup/rollup.esm.config.mjs @@ -1,5 +1,4 @@ import basicConfig from './rollup.config.mjs' -// import { terser } from "rollup-plugin-terser" import excludeDependenciesFromBundle from "rollup-plugin-exclude-dependencies-from-bundle" const config = { @@ -8,9 +7,6 @@ const config = { { file: 'dist/index.es.js', format: 'es', - // plugins: [ - // terser() - // ], }, ], plugins: [ diff --git a/webapp/packages/chat-sdk/rollup/rollup.umd.config.mjs b/webapp/packages/chat-sdk/rollup/rollup.umd.config.mjs index 5f408c308..062443458 100644 --- a/webapp/packages/chat-sdk/rollup/rollup.umd.config.mjs +++ b/webapp/packages/chat-sdk/rollup/rollup.umd.config.mjs @@ -1,5 +1,5 @@ import basicConfig from './rollup.config.mjs' -import { terser } from "@rollup/plugin-terser" +import { terser } from '@rollup/plugin-terser' import replace from '@rollup/plugin-replace' const config = { diff --git a/webapp/packages/chat-sdk/src/common/constants.ts b/webapp/packages/chat-sdk/src/common/constants.ts index 5d7e26a22..0e678feb7 100644 --- a/webapp/packages/chat-sdk/src/common/constants.ts +++ b/webapp/packages/chat-sdk/src/common/constants.ts @@ -42,9 +42,18 @@ export const THEME_COLOR_LIST = [ export const PARSE_ERROR_TIP = '小Q不太懂您说什么呐,回去一定补充知识'; +export const SEARCH_EXCEPTION_TIP = '查询出错啦,智能小Q还不够聪明,请您换个表达再试试'; + export const MSG_VALID_TIP = { [MsgValidTypeEnum.SEARCH_EXCEPTION]: '数据查询异常', [MsgValidTypeEnum.INVALID]: '小Q不太懂您说什么呐,回去一定补充知识', }; -export const PREFIX_CLS = 'ss-chat'; \ No newline at end of file +export const PREFIX_CLS = 'ss-chat'; + +export const AGG_TYPE_MAP = { + SUM: '总计', + AVG: '平均值', + MAX: '最大值', + MIN: '最小值', +} \ No newline at end of file diff --git a/webapp/packages/chat-sdk/src/common/type.ts b/webapp/packages/chat-sdk/src/common/type.ts index 69468cdc9..e4142e2b0 100644 --- a/webapp/packages/chat-sdk/src/common/type.ts +++ b/webapp/packages/chat-sdk/src/common/type.ts @@ -12,6 +12,8 @@ export type FieldType = { id: number; name: string; status: number; + domain: number; + type: string; value: string; }; @@ -33,7 +35,7 @@ export type EntityInfoType = { export type DateInfoType = { dateList: any[]; - dateMode: number; + dateMode: string; period: string; startDate: string; endDate: string; @@ -56,8 +58,11 @@ export type ChatContextType = { dateInfo: DateInfoType; dimensions: FieldType[]; metrics: FieldType[]; - entity: number; + entity: { alias: string[] }; + elementMatches: any[]; + queryMode: string; dimensionFilters: FilterItemType[]; + properties: any; }; export enum MsgValidTypeEnum { @@ -67,16 +72,11 @@ export enum MsgValidTypeEnum { INVALID = 3, }; -export type InstructionResonseType = { +export type PluginResonseType = { description: string; - instructionConfig: { - showElements: { elementId: string, params: any }[]; - showType: string; - relaShowElements: { elementId: string, params: any }[]; - relaShowType: string; - }; - instructionId: number; - instructionType: string; + webPage: { url: string, paramOptions: any, params: any, valueParams: any }; + pluginId: number; + pluginType: string; name: string; } @@ -103,9 +103,23 @@ export type MsgDataType = { queryId: number; queryMode: string; queryState: string; - response: InstructionResonseType; + response: PluginResonseType; }; +export enum ParseStateEnum { + COMPLETED = 'COMPLETED', + PENDING = 'PENDING', + FAILED = 'FAILED', +} + +export type ParseDataType = { + chatId: number; + queryText: string; + state: ParseStateEnum; + selectedParses: ChatContextType[]; + candidateParses: ChatContextType[]; +} + export type QueryDataType = { queryColumns: ColumnType[]; queryResults: any[]; @@ -120,7 +134,7 @@ export type ColumnType = { dataFormatType: string; dataFormat: { decimalPlaces: number; - needmultiply100: boolean; + needMultiply100: boolean; }; }; diff --git a/webapp/packages/chat-sdk/src/components/ChatItem/ExecuteItem.tsx b/webapp/packages/chat-sdk/src/components/ChatItem/ExecuteItem.tsx new file mode 100644 index 000000000..9e29fa3d2 --- /dev/null +++ b/webapp/packages/chat-sdk/src/components/ChatItem/ExecuteItem.tsx @@ -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 = ({ + question, + executeLoading, + chartIndex, + executeTip, + data, + isMobileMode, + triggerResize, + isLastMessage, + onSwitchEntity, + onChangeChart, +}) => { + const prefixCls = `${PREFIX_CLS}-item`; + + if (executeLoading) { + return ; + } + + if (executeTip) { + return ; + } + + if (!data || data.queryMode === 'WEB_PAGE') { + return null; + } + + const isMetricCard = + (data.queryMode === 'METRIC_DOMAIN' || data.queryMode === 'METRIC_FILTER') && + data.queryResults?.length === 1; + + return ( +
+ + {!isMetricCard && ( + + )} +
+ ); +}; + +export default ExecuteItem; diff --git a/webapp/packages/chat-sdk/src/components/ChatItem/ParseTip.tsx b/webapp/packages/chat-sdk/src/components/ChatItem/ParseTip.tsx new file mode 100644 index 000000000..4760f2cc5 --- /dev/null +++ b/webapp/packages/chat-sdk/src/components/ChatItem/ParseTip.tsx @@ -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 = ({ + parseLoading, + parseInfoOptions, + parseTip, + currentParseInfo, + onSelectParseInfo, +}) => { + const prefixCls = `${PREFIX_CLS}-item`; + + if (parseLoading) { + return ; + } + + if (parseTip) { + return ; + } + + 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 ( +
{ + if (isOptions && currentParseInfo === undefined) { + onSelectParseInfo(parseInfo); + } + }} + > + {index !== undefined &&
{index + 1}.
} + {!pluginName && isOptions &&
{modeName}:
} + {!!pluginName ? ( +
+ 将由问答插件 + {pluginName}来解答 +
+ ) : ( + <> + {queryMode === 'METRIC_ENTITY' || queryMode === 'ENTITY_DETAIL' ? ( +
+
{entityAlias}:
+
{entityName}
+
+ ) : ( +
+
主题域:
+
{domainName}
+
+ )} + {modeName === '算指标' && metric && ( +
+
指标:
+
{metric.name}
+
+ )} + {!isOptions && ( +
+
时间:
+
+ {startDate === endDate ? startDate : `${startDate} ~ ${endDate}`} +
+
+ )} + {['METRIC_GROUPBY', 'METRIC_ORDERBY', 'ENTITY_DETAIL'].includes(queryMode) && + fields && + fields.length > 0 && ( +
+
+ {queryMode === 'ENTITY_DETAIL' ? '查询字段' : '下钻维度'}: +
+
+ {fields + .slice(0, MAX_OPTION_VALUES_COUNT) + .map(field => field.name) + .join('、')} + {fields.length > MAX_OPTION_VALUES_COUNT && '...'} +
+
+ )} + {['METRIC_FILTER', 'METRIC_ENTITY', 'ENTITY_DETAIL', 'ENTITY_LIST_FILTER'].includes( + queryMode + ) && + dimensionFilters && + dimensionFilters?.length > 0 && ( +
+
筛选条件:
+ {dimensionFilters.slice(0, MAX_OPTION_VALUES_COUNT).map((filter, index) => ( +
+ {filter.name}: + + {Array.isArray(filter.value) ? filter.value.join('、') : filter.value} + + {index !== dimensionFilters.length - 1 && } +
+ ))} + {dimensionFilters.length > MAX_OPTION_VALUES_COUNT && '...'} +
+ )} + {queryMode === 'METRIC_ORDERBY' && aggType && aggType !== 'NONE' && ( +
+
聚合方式:
+
{AGG_TYPE_MAP[aggType]}
+
+ )} + + )} +
+ ); + }; + + let tipNode: ReactNode; + + if (parseInfoOptions.length > 1) { + tipNode = ( +
+
您的问题解析为以下几项,请您点击确认
+
+ {parseInfoOptions.map((item, index) => getTipNode(item, true, index))} +
+
+ ); + } else { + const pluginName = parseInfoOptions[0]?.properties?.CONTEXT?.plugin?.name; + tipNode = ( +
+
{!!pluginName ? '您的问题' : '您的问题解析为:'}
+ {getTipNode(parseInfoOptions[0])} +
+ ); + } + + return ; +}; + +export default ParseTip; diff --git a/webapp/packages/chat-sdk/src/components/ChatItem/index.tsx b/webapp/packages/chat-sdk/src/components/ChatItem/index.tsx index 8dfe17a3d..acebd42dc 100644 --- a/webapp/packages/chat-sdk/src/components/ChatItem/index.tsx +++ b/webapp/packages/chat-sdk/src/components/ChatItem/index.tsx @@ -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 = ({ msg, - followQuestions, conversationId, domainId, + filter, isLastMessage, isMobileMode, triggerResize, msgData, onMsgDataLoaded, + onUpdateMessageScroll, }) => { const [data, setData] = useState(); - const [loading, setLoading] = useState(false); - const [tip, setTip] = useState(''); + const [parseLoading, setParseLoading] = useState(false); + const [parseInfo, setParseInfo] = useState(); + const [parseInfoOptions, setParseInfoOptions] = useState([]); + 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) => { 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 = ({ 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 ( -
- - -
- ); - } + const onChangeChart = () => { + setChartIndex(chartIndex + 1); + }; - if (tip) { - return ( -
- - -
- ); - } - - 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 (
- -
- - {!isMetricCard && ( - - )} +
+ +
+ +
+ {executeMode && data?.queryMode !== 'WEB_PAGE' && ( +
+ +
+ +
+
+ )}
); }; diff --git a/webapp/packages/chat-sdk/src/components/ChatItem/style.less b/webapp/packages/chat-sdk/src/components/ChatItem/style.less index 68d68deb7..1dee38081 100644 --- a/webapp/packages/chat-sdk/src/components/ChatItem/style.less +++ b/webapp/packages/chat-sdk/src/components/ChatItem/style.less @@ -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 { diff --git a/webapp/packages/chat-sdk/src/components/ChatMsg/Bar/index.tsx b/webapp/packages/chat-sdk/src/components/ChatMsg/Bar/index.tsx index 3ef6320c1..d807127ef 100644 --- a/webapp/packages/chat-sdk/src/components/ChatMsg/Bar/index.tsx +++ b/webapp/packages/chat-sdk/src/components/ChatMsg/Bar/index.tsx @@ -31,7 +31,7 @@ const BarChart: React.FC = ({ const { queryColumns, queryResults, entityInfo, chatContext, queryMode } = data; - const { dateInfo } = chatContext || {}; + const { dateInfo, dimensionFilters } = chatContext || {}; const categoryColumnName = queryColumns?.find(column => column.showType === 'CATEGORY')?.nameEn || ''; @@ -51,13 +51,6 @@ const BarChart: React.FC = ({ ); const xData = data.map(item => item[categoryColumnName]); instanceObj.setOption({ - // legend: { - // left: 0, - // top: 0, - // icon: 'rect', - // itemWidth: 15, - // itemHeight: 5, - // }, xAxis: { type: 'category', axisTick: { @@ -166,21 +159,43 @@ const BarChart: React.FC = ({ ); } + const hasFilterSection = dimensionFilters?.length > 0; + + const prefixCls = `${PREFIX_CLS}-bar`; + return (
-
{metricColumn?.name}
- +
+
{metricColumn?.name}
+ {(hasFilterSection || drillDownDimension) && ( +
+ ( +
+ + {drillDownDimension && ( +
+
下钻维度:
+
{drillDownDimension.name}
+
+ )} +
+ ) +
+ )} +
{dateInfo && ( -
+
{dateInfo.startDate === dateInfo.endDate ? dateInfo.startDate : `${dateInfo.startDate} ~ ${dateInfo.endDate}`}
)} -
+
- {(queryMode === 'METRIC_DOMAIN' || queryMode === 'METRIC_FILTER') && ( + {(queryMode === 'METRIC_DOMAIN' || + queryMode === 'METRIC_FILTER' || + queryMode === 'METRIC_GROUPBY') && ( = ({ chatContext }) => { +const FilterSection: React.FC = ({ chatContext, entityInfo }) => { const prefixCls = `${PREFIX_CLS}-filter-section`; + const entityInfoList = + entityInfo?.dimensions?.filter(dimension => !dimension.bizName.includes('photo')) || []; + const { dimensionFilters } = chatContext || {}; const hasFilterSection = dimensionFilters && dimensionFilters.length > 0; @@ -16,7 +20,7 @@ const FilterSection: React.FC = ({ chatContext }) => {
筛选条件:
- {dimensionFilters.map(filterItem => { + {(entityInfoList.length > 0 ? entityInfoList : dimensionFilters).map(filterItem => { const filterValue = typeof filterItem.value === 'string' ? [filterItem.value] : filterItem.value || []; return ( diff --git a/webapp/packages/chat-sdk/src/components/ChatMsg/FilterSection/style.less b/webapp/packages/chat-sdk/src/components/ChatMsg/FilterSection/style.less index c18d4606a..111a6e457 100644 --- a/webapp/packages/chat-sdk/src/components/ChatMsg/FilterSection/style.less +++ b/webapp/packages/chat-sdk/src/components/ChatMsg/FilterSection/style.less @@ -5,21 +5,25 @@ .@{filter-section-prefix-cls} { display: flex; align-items: center; + flex-wrap: wrap; + row-gap: 12px; color: var(--text-color-secondary); font-weight: normal; font-size: 13px; + &-field-label { + color: var(--text-color-fourth); + } + &-filter-values { display: flex; align-items: center; - column-gap: 6px; + flex-wrap: wrap; + column-gap: 12px; } &-filter-item { - padding: 2px 12px; color: var(--text-color-third); - background-color: #edf2f2; - border-radius: 13px; max-width: 200px; text-overflow: ellipsis; white-space: nowrap; diff --git a/webapp/packages/chat-sdk/src/components/ChatMsg/Message/index.tsx b/webapp/packages/chat-sdk/src/components/ChatMsg/Message/index.tsx index 0a76ee136..fbb7fb5bd 100644 --- a/webapp/packages/chat-sdk/src/components/ChatMsg/Message/index.tsx +++ b/webapp/packages/chat-sdk/src/components/ChatMsg/Message/index.tsx @@ -12,67 +12,28 @@ type Props = { entityInfo?: EntityInfoType; children?: React.ReactNode; isMobileMode?: boolean; + queryMode?: string; }; const Message: React.FC = ({ - position, width, height, - title, - followQuestions, children, bubbleClassName, - chatContext, entityInfo, - isMobileMode, + queryMode, + chatContext, }) => { - const { dimensionFilters, domainName } = chatContext || {}; - const prefixCls = `${PREFIX_CLS}-message`; + const { domainName, dateInfo, dimensionFilters } = chatContext || {}; + const { startDate, endDate } = dateInfo || {}; + const entityInfoList = entityInfo?.dimensions?.filter(dimension => !dimension.bizName.includes('photo')) || []; - const hasFilterSection = - dimensionFilters && dimensionFilters.length > 0 && entityInfoList.length === 0; - - const filterSection = hasFilterSection && ( -
-
筛选条件:
-
- {dimensionFilters.map(filterItem => { - const filterValue = - typeof filterItem.value === 'string' ? [filterItem.value] : filterItem.value || []; - return ( -
- {filterItem.name}:{filterValue.join('、')} -
- ); - })} -
-
- ); - - const leftTitle = title - ? followQuestions && followQuestions.length > 0 - ? `多轮对话:${[title, ...followQuestions].join(' ← ')}` - : `单轮对话:${title}` - : ''; - return (
-
- {domainName &&
{domainName}
} - {position === 'left' && leftTitle && ( -
- ({leftTitle}) -
- )} -
= ({ e.stopPropagation(); }} > - {entityInfoList.length > 0 && ( -
- {/* {filterSection} */} - {entityInfoList.length > 0 && ( + {(queryMode === 'METRIC_ENTITY' || queryMode === 'ENTITY_DETAIL') && + entityInfoList.length > 0 && ( +
{entityInfoList.slice(0, 4).map(dimension => { return ( @@ -100,7 +60,36 @@ const Message: React.FC = ({ ); })}
- )} +
+ )} + {queryMode === 'ENTITY_LIST_FILTER' && ( +
+
+
+
主题域:
+
{domainName}
+
+
+
时间:
+
+ {startDate === endDate ? startDate : `${startDate} ~ ${endDate}`} +
+
+ {dimensionFilters && dimensionFilters?.length > 0 && ( +
+
筛选条件:
+ {dimensionFilters.map((filter, index) => ( +
+ {filter.name}: + + {Array.isArray(filter.value) ? filter.value.join('、') : filter.value} + + {index !== dimensionFilters.length - 1 && } +
+ ))} +
+ )} +
)}
{children}
diff --git a/webapp/packages/chat-sdk/src/components/ChatMsg/Message/style.less b/webapp/packages/chat-sdk/src/components/ChatMsg/Message/style.less index f7ffa03c5..5c3f737f9 100644 --- a/webapp/packages/chat-sdk/src/components/ChatMsg/Message/style.less +++ b/webapp/packages/chat-sdk/src/components/ChatMsg/Message/style.less @@ -83,7 +83,8 @@ align-items: center; row-gap: 12px; flex-wrap: wrap; - margin-top: 4px; + margin-top: 2px; + margin-bottom: 12px; column-gap: 20px; color: var(--text-color-secondary); background: rgba(133, 156, 241, 0.1); @@ -96,7 +97,6 @@ display: flex; flex-wrap: wrap; align-items: center; - font-size: 13px; column-gap: 20px; row-gap: 10px; } @@ -107,11 +107,12 @@ } &-info-name { - color: var(--text-color-third); + color: var(--text-color-secondary); } &-info-value { color: var(--text-color); + font-weight: 500; } } diff --git a/webapp/packages/chat-sdk/src/components/ChatMsg/MetricCard/index.tsx b/webapp/packages/chat-sdk/src/components/ChatMsg/MetricCard/index.tsx index 437747c9f..3b6b48fe5 100644 --- a/webapp/packages/chat-sdk/src/components/ChatMsg/MetricCard/index.tsx +++ b/webapp/packages/chat-sdk/src/components/ChatMsg/MetricCard/index.tsx @@ -1,11 +1,12 @@ import { PREFIX_CLS } from '../../../common/constants'; -import { formatByThousandSeperator } from '../../../utils/utils'; +import { formatMetric } from '../../../utils/utils'; import ApplyAuth from '../ApplyAuth'; import { DrillDownDimensionType, MsgDataType } from '../../../common/type'; import PeriodCompareItem from './PeriodCompareItem'; import DrillDownDimensions from '../../DrillDownDimensions'; import { Spin } from 'antd'; import classNames from 'classnames'; +import FilterSection from '../FilterSection'; type Props = { data: MsgDataType; @@ -25,8 +26,8 @@ const MetricCard: React.FC = ({ const { queryMode, queryColumns, queryResults, entityInfo, aggregateInfo, chatContext } = data; const { metricInfos } = aggregateInfo || {}; - const { dateInfo } = chatContext || {}; - const { startDate, endDate } = dateInfo || {}; + const { dateInfo, dimensionFilters } = chatContext || {}; + const { startDate } = dateInfo || {}; const indicatorColumn = queryColumns?.find(column => column.showType === 'NUMBER'); const indicatorColumnName = indicatorColumn?.nameEn || ''; @@ -37,19 +38,36 @@ const MetricCard: React.FC = ({ [`${prefixCls}-indicator-period-compare`]: metricInfos?.length > 0, }); + const hasFilterSection = dimensionFilters?.length > 0; + return (
-
{indicatorColumn?.name}
+
+
{indicatorColumn?.name}
+ {(hasFilterSection || drillDownDimension) && ( +
+ ( +
+ + {drillDownDimension && ( +
+
下钻维度:
+
{drillDownDimension.name}
+
+ )} +
+ ) +
+ )} +
-
- {startDate === endDate ? startDate : `${startDate} ~ ${endDate}`} -
+
{startDate}
{indicatorColumn && !indicatorColumn?.authorized ? ( ) : (
- {formatByThousandSeperator(queryResults?.[0]?.[indicatorColumnName])} + {formatMetric(queryResults?.[0]?.[indicatorColumnName]) || '-'}
)} {metricInfos?.length > 0 && ( diff --git a/webapp/packages/chat-sdk/src/components/ChatMsg/MetricCard/style.less b/webapp/packages/chat-sdk/src/components/ChatMsg/MetricCard/style.less index 26685e1af..3fa55d1d9 100644 --- a/webapp/packages/chat-sdk/src/components/ChatMsg/MetricCard/style.less +++ b/webapp/packages/chat-sdk/src/components/ChatMsg/MetricCard/style.less @@ -7,6 +7,41 @@ height: 130px; row-gap: 4px; + &-top-bar { + display: flex; + align-items: baseline; + flex-wrap: wrap; + column-gap: 8px; + } + + &-filter-section-wrapper { + display: flex; + align-items: center; + color: var(--text-color-third); + } + + &-filter-section { + display: flex; + align-items: center; + font-size: 13px; + column-gap: 12px; + color: var(--text-color-third); + } + + &-filter-item { + display: flex; + align-items: center; + } + + &-filter-item-label { + color: var(--text-color-third); + } + + &-filter-item-value { + color: var(--text-color); + font-weight: 500; + } + &-indicator-name { font-size: 14px; color: var(--text-color); @@ -74,7 +109,7 @@ &-drill-down-dimensions { position: absolute; - bottom: -38px; - left: 0; + bottom: -44px; + left: -16; } } \ No newline at end of file diff --git a/webapp/packages/chat-sdk/src/components/ChatMsg/MetricTrend/MetricInfo.tsx b/webapp/packages/chat-sdk/src/components/ChatMsg/MetricTrend/MetricInfo.tsx index c67f11a8c..acf636869 100644 --- a/webapp/packages/chat-sdk/src/components/ChatMsg/MetricTrend/MetricInfo.tsx +++ b/webapp/packages/chat-sdk/src/components/ChatMsg/MetricTrend/MetricInfo.tsx @@ -1,5 +1,5 @@ import { PREFIX_CLS } from '../../../common/constants'; -import { formatByThousandSeperator } from '../../../utils/utils'; +import { formatMetric } from '../../../utils/utils'; import { AggregateInfoType } from '../../../common/type'; import PeriodCompareItem from '../MetricCard/PeriodCompareItem'; @@ -18,7 +18,7 @@ const MetricInfo: React.FC = ({ aggregateInfo }) => {
{date}
-
{formatByThousandSeperator(value)}
+
{formatMetric(value)}
{metricInfos?.length > 0 && (
{Object.keys(statistics).map((key: any) => ( diff --git a/webapp/packages/chat-sdk/src/components/ChatMsg/MetricTrend/MetricTrendChart.tsx b/webapp/packages/chat-sdk/src/components/ChatMsg/MetricTrend/MetricTrendChart.tsx index 692171d18..4022fce3b 100644 --- a/webapp/packages/chat-sdk/src/components/ChatMsg/MetricTrend/MetricTrendChart.tsx +++ b/webapp/packages/chat-sdk/src/components/ChatMsg/MetricTrend/MetricTrendChart.tsx @@ -46,8 +46,17 @@ const MetricTrendChart: React.FC = ({ } const valueColumnName = metricField.nameEn; - const groupDataValue = groupByColumn(resultList, categoryColumnName); - const [startDate, endDate] = getMinMaxDate(resultList, dateColumnName); + const dataSource = resultList.map((item: any) => { + return { + ...item, + [dateColumnName]: Array.isArray(item[dateColumnName]) + ? moment(item[dateColumnName].join('')).format('MM-DD') + : item[dateColumnName], + }; + }); + + const groupDataValue = groupByColumn(dataSource, categoryColumnName); + const [startDate, endDate] = getMinMaxDate(dataSource, dateColumnName); const groupData = Object.keys(groupDataValue).reduce((result: any, key) => { result[key] = startDate && @@ -61,7 +70,7 @@ const MetricTrendChart: React.FC = ({ endDate, dateColumnName.includes('month') ? 'months' : 'days' ) - : groupDataValue[key].reverse(); + : groupDataValue[key]; return result; }, {}); @@ -167,7 +176,7 @@ const MetricTrendChart: React.FC = ({ data: data.map((item: any) => { const value = item[valueColumnName]; return metricField.dataFormatType === 'percent' && - metricField.dataFormat?.needmultiply100 + metricField.dataFormat?.needMultiply100 ? value * 100 : value; }), diff --git a/webapp/packages/chat-sdk/src/components/ChatMsg/MetricTrend/index.tsx b/webapp/packages/chat-sdk/src/components/ChatMsg/MetricTrend/index.tsx index 0389f3ecd..e32d1abc1 100644 --- a/webapp/packages/chat-sdk/src/components/ChatMsg/MetricTrend/index.tsx +++ b/webapp/packages/chat-sdk/src/components/ChatMsg/MetricTrend/index.tsx @@ -10,29 +10,33 @@ import Table from '../Table'; import DrillDownDimensions from '../../DrillDownDimensions'; import MetricInfo from './MetricInfo'; import FilterSection from '../FilterSection'; -import moment from 'moment'; type Props = { data: MsgDataType; + chartIndex: number; triggerResize?: boolean; onApplyAuth?: (domain: string) => void; }; -const MetricTrend: React.FC = ({ data, triggerResize, onApplyAuth }) => { +const MetricTrend: React.FC = ({ data, chartIndex, triggerResize, onApplyAuth }) => { const { queryColumns, queryResults, entityInfo, chatContext, queryMode, aggregateInfo } = data; - const dateOptions = DATE_TYPES[chatContext?.dateInfo?.period] || DATE_TYPES.DAY; - const initialDateOption = dateOptions.find( - (option: any) => option.value === chatContext?.dateInfo?.unit - )?.value; + const { dateMode, unit } = chatContext?.dateInfo || {}; - const [columns, setColumns] = useState(queryColumns); + const dateOptions = DATE_TYPES[chatContext?.dateInfo?.period] || DATE_TYPES.DAY; + const initialDateOption = dateOptions.find((option: any) => { + return dateMode === 'RECENT' && option.value === unit; + })?.value; + + const [columns, setColumns] = useState(queryColumns || []); const currentMetricField = columns.find((column: any) => column.showType === 'NUMBER'); const [activeMetricField, setActiveMetricField] = useState(chatContext.metrics?.[0]); const [dataSource, setDataSource] = useState(queryResults); const [currentDateOption, setCurrentDateOption] = useState(initialDateOption); + const [dimensions, setDimensions] = useState(chatContext?.dimensions); const [drillDownDimension, setDrillDownDimension] = useState(); + const [dateModeValue, setDateModeValue] = useState(dateMode); const [loading, setLoading] = useState(false); const dateField: any = columns.find( @@ -46,6 +50,18 @@ const MetricTrend: React.FC = ({ data, triggerResize, onApplyAuth }) => { setDataSource(queryResults); }, [queryResults]); + useEffect(() => { + if (queryMode === 'METRIC_GROUPBY') { + const dimensionValue = chatContext?.dimensions?.find( + dimension => dimension.type === 'DIMENSION' + ); + setDrillDownDimension(dimensionValue); + setDimensions( + chatContext?.dimensions?.filter(dimension => dimension.id !== dimensionValue?.id) + ); + } + }, []); + const onLoadData = async (value: any) => { setLoading(true); const { data } = await queryData({ @@ -61,19 +77,13 @@ const MetricTrend: React.FC = ({ data, triggerResize, onApplyAuth }) => { const selectDateOption = (dateOption: number) => { setCurrentDateOption(dateOption); - const endDate = moment().subtract(1, 'days').format('YYYY-MM-DD'); - const startDate = moment(endDate) - .subtract(dateOption - 1, 'days') - .format('YYYY-MM-DD'); + setDateModeValue('RECENT'); onLoadData({ metrics: [activeMetricField], - dimensions: drillDownDimension - ? [...(chatContext.dimensions || []), drillDownDimension] - : undefined, + dimensions: drillDownDimension ? [...(dimensions || []), drillDownDimension] : undefined, dateInfo: { ...chatContext?.dateInfo, - startDate, - endDate, + dateMode: 'RECENT', unit: dateOption, }, }); @@ -82,10 +92,12 @@ const MetricTrend: React.FC = ({ data, triggerResize, onApplyAuth }) => { const onSwitchMetric = (metricField: FieldType) => { setActiveMetricField(metricField); onLoadData({ - dateInfo: { ...chatContext.dateInfo, unit: currentDateOption || chatContext.dateInfo.unit }, - dimensions: drillDownDimension - ? [...(chatContext.dimensions || []), drillDownDimension] - : undefined, + dateInfo: { + ...chatContext.dateInfo, + dateMode: dateModeValue, + unit: currentDateOption || chatContext.dateInfo.unit, + }, + dimensions: drillDownDimension ? [...(dimensions || []), drillDownDimension] : undefined, metrics: [metricField], }); }; @@ -93,10 +105,13 @@ const MetricTrend: React.FC = ({ data, triggerResize, onApplyAuth }) => { const onSelectDimension = (dimension?: DrillDownDimensionType) => { setDrillDownDimension(dimension); onLoadData({ - dateInfo: { ...chatContext.dateInfo, unit: currentDateOption || chatContext.dateInfo.unit }, + dateInfo: { + ...chatContext.dateInfo, + dateMode: dateModeValue, + unit: currentDateOption || chatContext.dateInfo.unit, + }, metrics: [activeMetricField], - dimensions: - dimension === undefined ? undefined : [...(chatContext.dimensions || []), dimension], + dimensions: dimension === undefined ? undefined : [...(dimensions || []), dimension], }); }; @@ -106,36 +121,58 @@ const MetricTrend: React.FC = ({ data, triggerResize, onApplyAuth }) => { const prefixCls = `${CLS_PREFIX}-metric-trend`; + const { dimensionFilters } = chatContext || {}; + + const hasFilterSection = dimensionFilters?.length > 0; + return (
- {chatContext.metrics.length > 0 && ( -
- {chatContext.metrics.map((metricField: FieldType) => { - const metricFieldClass = classNames(`${prefixCls}-metric-field`, { - [`${prefixCls}-metric-field-active`]: - activeMetricField?.bizName === metricField.bizName && - chatContext.metrics.length > 1, - [`${prefixCls}-metric-field-single`]: chatContext.metrics.length === 1, - }); - return ( -
{ - if (chatContext.metrics.length > 1) { - onSwitchMetric(metricField); - } - }} - > - {metricField.name} -
- ); - })} -
- )} +
+ {chatContext.metrics.length > 0 && ( +
+ {chatContext.metrics.slice(0, 5).map((metricField: FieldType) => { + const metricFieldClass = classNames(`${prefixCls}-metric-field`, { + [`${prefixCls}-metric-field-active`]: + activeMetricField?.bizName === metricField.bizName && + chatContext.metrics.length > 1, + [`${prefixCls}-metric-field-single`]: chatContext.metrics.length === 1, + }); + return ( +
{ + if (chatContext.metrics.length > 1) { + onSwitchMetric(metricField); + } + }} + > + {metricField.name} +
+ ); + })} +
+ )} + {(hasFilterSection || drillDownDimension) && ( +
+ ( +
+ + {drillDownDimension && ( +
+
下钻维度:
+
+ {drillDownDimension.name} +
+
+ )} +
+ ) +
+ )} +
{aggregateInfo?.metricInfos?.length > 0 && } -
{dateOptions.map((dateOption: { label: string; value: number }, index: number) => { const dateOptionClass = classNames(`${prefixCls}-date-option`, { @@ -164,7 +201,7 @@ const MetricTrend: React.FC = ({ data, triggerResize, onApplyAuth }) => { })}
- {dataSource?.length === 1 ? ( + {dataSource?.length === 1 || chartIndex % 2 === 1 ? ( ) : ( = ({ data, triggerResize, onApplyAuth }) => { /> )} - {(queryMode === 'METRIC_DOMAIN' || queryMode === 'METRIC_FILTER') && ( + {(queryMode === 'METRIC_DOMAIN' || + queryMode === 'METRIC_FILTER' || + queryMode === 'METRIC_GROUPBY') && ( = ({ infoType = SemanticTypeEnum.METRIC }) => { - return ( - - {SEMANTIC_TYPE_MAP[infoType]} - - ); -}; - -export default SemanticTypeTag; diff --git a/webapp/packages/chat-sdk/src/components/ChatMsg/SemanticInfoPopover/index.tsx b/webapp/packages/chat-sdk/src/components/ChatMsg/SemanticInfoPopover/index.tsx deleted file mode 100644 index a4882c205..000000000 --- a/webapp/packages/chat-sdk/src/components/ChatMsg/SemanticInfoPopover/index.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { Popover, message, Row, Col, Button, Spin } from 'antd'; -import React, { useEffect, useState } from 'react'; -import { SemanticTypeEnum } from '../../../common/type'; -import { queryMetricInfo } from '../../../service'; -import SemanticTypeTag from './SemanticTypeTag'; -import { isMobile } from '../../../utils/utils'; -import { CLS_PREFIX } from '../../../common/constants'; - -type Props = { - children: React.ReactNode; - classId?: number; - infoType?: SemanticTypeEnum; - uniqueId: string | number; - onDetailBtnClick?: (data: any) => void; -}; - -const SemanticInfoPopover: React.FC = ({ - classId, - infoType, - uniqueId, - children, - onDetailBtnClick, -}) => { - const [semanticInfo, setSemanticInfo] = useState(undefined); - const [popoverVisible, setPopoverVisible] = useState(false); - const [loading, setLoading] = useState(false); - - const prefixCls = `${CLS_PREFIX}-semantic-info-popover`; - - const text = ( - - - - - {onDetailBtnClick && ( - - {semanticInfo && ( - - )} - - )} - - ); - - const content = loading ? ( -
- -
- ) : ( -
- {semanticInfo?.description || '暂无数据'} -
- ); - - const getMetricInfo = async () => { - setLoading(true); - const { data: resData } = await queryMetricInfo({ - classId, - uniqueId, - }); - const { code, data, msg } = resData; - setLoading(false); - if (code === '0') { - setSemanticInfo({ - ...data, - semanticInfoType: SemanticTypeEnum.METRIC, - }); - } else { - message.error(msg); - } - }; - - useEffect(() => { - if (popoverVisible && !semanticInfo) { - getMetricInfo(); - } - }, [popoverVisible]); - - return ( - { - setPopoverVisible(visible); - }} - overlayClassName={prefixCls} - > - {children} - - ); -}; - -export default SemanticInfoPopover; diff --git a/webapp/packages/chat-sdk/src/components/ChatMsg/SemanticInfoPopover/style.less b/webapp/packages/chat-sdk/src/components/ChatMsg/SemanticInfoPopover/style.less deleted file mode 100644 index 6dbd4e99c..000000000 --- a/webapp/packages/chat-sdk/src/components/ChatMsg/SemanticInfoPopover/style.less +++ /dev/null @@ -1,18 +0,0 @@ -@import '../../../styles/index.less'; - -@semantic-info-popover-cls: ~'@{supersonic-chat-prefix}-semantic-info-popover'; - -.semantic-info-popover-cls { - max-width: 300px; - &-spin-box { - text-align: center; - padding-top: 10px; - } - .ant-popover-title{ - padding: 5px 8px 4px; - } - .ant-popover-inner-content { - min-height: 60px; - min-width: 185px; - } -} diff --git a/webapp/packages/chat-sdk/src/components/ChatMsg/Table/index.tsx b/webapp/packages/chat-sdk/src/components/ChatMsg/Table/index.tsx index b7f1e48f8..fb30fd42c 100644 --- a/webapp/packages/chat-sdk/src/components/ChatMsg/Table/index.tsx +++ b/webapp/packages/chat-sdk/src/components/ChatMsg/Table/index.tsx @@ -3,13 +3,15 @@ import { Table as AntTable } from 'antd'; import { MsgDataType } from '../../../common/type'; import { CLS_PREFIX } from '../../../common/constants'; import ApplyAuth from '../ApplyAuth'; +import { SizeType } from 'antd/es/config-provider/SizeContext'; type Props = { data: MsgDataType; + size?: SizeType; onApplyAuth?: (domain: string) => void; }; -const Table: React.FC = ({ data, onApplyAuth }) => { +const Table: React.FC = ({ data, size, onApplyAuth }) => { const { entityInfo, queryColumns, queryResults } = data; const prefixCls = `${CLS_PREFIX}-table`; @@ -19,7 +21,7 @@ const Table: React.FC = ({ data, onApplyAuth }) => { return { dataIndex: nameEn, key: nameEn, - title: name, + title: name || nameEn, render: (value: string | number) => { if (!authorized) { return ( @@ -30,7 +32,7 @@ const Table: React.FC = ({ data, onApplyAuth }) => { return (
{`${formatByDecimalPlaces( - dataFormat?.needmultiply100 ? +value * 100 : value, + dataFormat?.needMultiply100 ? +value * 100 : value, dataFormat?.decimalPlaces || 2 )}%`}
@@ -71,6 +73,7 @@ const Table: React.FC = ({ data, onApplyAuth }) => { style={{ width: '100%' }} scroll={{ x: 'max-content' }} rowClassName={getRowClassName} + size={size} /> ); diff --git a/webapp/packages/chat-sdk/src/components/ChatMsg/index.tsx b/webapp/packages/chat-sdk/src/components/ChatMsg/index.tsx index 4ba05a9c3..1496c3abe 100644 --- a/webapp/packages/chat-sdk/src/components/ChatMsg/index.tsx +++ b/webapp/packages/chat-sdk/src/components/ChatMsg/index.tsx @@ -10,19 +10,13 @@ import { queryData } from '../../service'; type Props = { question: string; - followQuestions?: string[]; data: MsgDataType; + chartIndex: number; isMobileMode?: boolean; triggerResize?: boolean; }; -const ChatMsg: React.FC = ({ - question, - followQuestions, - data, - isMobileMode, - triggerResize, -}) => { +const ChatMsg: React.FC = ({ question, data, chartIndex, isMobileMode, triggerResize }) => { const { queryColumns, queryResults, chatContext, entityInfo, queryMode } = data; const [columns, setColumns] = useState(queryColumns); @@ -41,7 +35,10 @@ const ChatMsg: React.FC = ({ const metricFields = columns.filter(item => item.showType === 'NUMBER'); const isMetricCard = - (queryMode === 'METRIC_DOMAIN' || queryMode === 'METRIC_FILTER') && singleData; + (queryMode === 'METRIC_DOMAIN' || queryMode === 'METRIC_FILTER') && + (singleData || chatContext?.dateInfo?.startDate === chatContext?.dateInfo?.endDate); + + const isText = columns.length === 1 && columns[0].showType === 'CATEGORY' && singleData; const onLoadData = async (value: any) => { setLoading(true); @@ -65,6 +62,13 @@ const ChatMsg: React.FC = ({ }; const getMsgContent = () => { + if (isText) { + return ( +
+ {dataSource[0][columns[0].nameEn]} +
+ ); + } if (isMetricCard) { return ( = ({ return ( ); @@ -105,7 +110,9 @@ const ChatMsg: React.FC = ({ }; let width = '100%'; - if (isMetricCard) { + if (isText) { + width = 'fit-content'; + } else if (isMetricCard) { width = '370px'; } else if (categoryField.length > 1 && !isMobile && !isMobileMode) { if (columns.length === 1) { @@ -121,9 +128,9 @@ const ChatMsg: React.FC = ({ chatContext={chatContext} entityInfo={entityInfo} title={question} - followQuestions={followQuestions} isMobileMode={isMobileMode} width={width} + queryMode={queryMode} > {getMsgContent()} diff --git a/webapp/packages/chat-sdk/src/components/DrillDownDimensions/index.tsx b/webapp/packages/chat-sdk/src/components/DrillDownDimensions/index.tsx index 0fdd7714b..186d9a61d 100644 --- a/webapp/packages/chat-sdk/src/components/DrillDownDimensions/index.tsx +++ b/webapp/packages/chat-sdk/src/components/DrillDownDimensions/index.tsx @@ -14,7 +14,6 @@ type Props = { onSelectDimension: (dimension?: DrillDownDimensionType) => void; }; -const DEFAULT_DIMENSION_COUNT = 5; const MAX_DIMENSION_COUNT = 20; const DrillDownDimensions: React.FC = ({ @@ -26,6 +25,8 @@ const DrillDownDimensions: React.FC = ({ }) => { const [dimensions, setDimensions] = useState([]); + const DEFAULT_DIMENSION_COUNT = isMetricCard ? 3 : 5; + const prefixCls = `${CLS_PREFIX}-drill-down-dimensions`; const initData = async () => { diff --git a/webapp/packages/chat-sdk/src/components/RecommendOptions/index.tsx b/webapp/packages/chat-sdk/src/components/RecommendOptions/index.tsx new file mode 100644 index 000000000..0ef2c2a34 --- /dev/null +++ b/webapp/packages/chat-sdk/src/components/RecommendOptions/index.tsx @@ -0,0 +1,134 @@ +import { getFormattedValue, isMobile } from '../../utils/utils'; +import { Table } from 'antd'; +import Avatar from 'antd/lib/avatar/avatar'; +import moment from 'moment'; +import { useEffect, useState } from 'react'; +import { queryEntities } from '../../service'; +import { CLS_PREFIX } from '../../common/constants'; +import IconFont from '../IconFont'; +import classNames from 'classnames'; + +type Props = { + entityId: string | number; + domainId: number; + domainName: string; + isMobileMode?: boolean; + onSelect: (option: string) => void; +}; + +const RecommendOptions: React.FC = ({ + entityId, + domainId, + domainName, + isMobileMode, + onSelect, +}) => { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + + const prefixCls = `${CLS_PREFIX}-recommend-options`; + + const initData = async () => { + setLoading(true); + const res = await queryEntities(entityId, domainId); + setLoading(false); + setData(res.data.data); + }; + + useEffect(() => { + if (entityId) { + initData(); + } + }, [entityId]); + + const getSectionOptions = () => { + const basicColumn = { + dataIndex: 'name', + key: 'name', + title: '基本信息', + render: (name: string, record: any) => { + return ( +
+ } + src={record.url} + /> +
+ {name} + {record.ver && record.ver !== '完整版' && record.ver !== '-' && `(${record.ver})`} + {record.singerName && ` - ${record.singerName}`} +
+
+ ); + }, + }; + + const playCntColumnIdex = domainName.includes('歌曲') + ? 'tme3platAvgLogYyPlayCnt' + : 'tme3platJsPlayCnt'; + + const columns = isMobile + ? [basicColumn] + : [ + basicColumn, + domainName.includes('艺人') + ? { + dataIndex: 'onlineSongCnt', + key: 'onlineSongCnt', + title: '在架歌曲数', + align: 'center', + render: (onlineSongCnt: string) => { + return onlineSongCnt ? getFormattedValue(+onlineSongCnt) : '-'; + }, + } + : { + dataIndex: 'publishTime', + key: 'publishTime', + title: '发布时间', + align: 'center', + render: (publishTime: string) => { + return publishTime ? moment(publishTime).format('YYYY-MM-DD') : '-'; + }, + }, + { + dataIndex: playCntColumnIdex, + key: playCntColumnIdex, + align: 'center', + title: domainName.includes('歌曲') ? '近7天日均运营播放量' : '昨日结算播放量', + render: (value: string) => { + return value ? getFormattedValue(+value) : '-'; + }, + }, + ]; + + return ( +
{ + return { + onClick: () => { + onSelect(record.id); + }, + }; + }} + /> + ); + }; + + const recommendOptionsClass = classNames(prefixCls, { + [`${prefixCls}-mobile-mode`]: isMobileMode, + }); + + return
{getSectionOptions()}
; +}; + +export default RecommendOptions; diff --git a/webapp/packages/chat-sdk/src/components/RecommendOptions/style.less b/webapp/packages/chat-sdk/src/components/RecommendOptions/style.less new file mode 100644 index 000000000..9b1e5877c --- /dev/null +++ b/webapp/packages/chat-sdk/src/components/RecommendOptions/style.less @@ -0,0 +1,24 @@ +@import '../../styles/index.less'; + +@recommend-options-prefix-cls: ~'@{supersonic-chat-prefix}-recommend-options'; + +.@{recommend-options-prefix-cls} { + padding: 8px 0 12px; + + &-item-name-column { + display: flex; + align-items: center; + column-gap: 6px; + } + + &-entity-name { + &:hover { + color: var(--primary-color); + } + } + + &-table-row { + cursor: pointer; + } +} + diff --git a/webapp/packages/chat-sdk/src/components/Suggestion/index.tsx b/webapp/packages/chat-sdk/src/components/Suggestion/index.tsx deleted file mode 100644 index 388f6c858..000000000 --- a/webapp/packages/chat-sdk/src/components/Suggestion/index.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { isMobile } from '../../utils/utils'; -import { ReloadOutlined } from '@ant-design/icons'; -import classNames from 'classnames'; -import { useEffect, useState } from 'react'; -import { EntityInfoType } from '../../common/type'; -import Message from '../ChatMsg/Message'; -import { CLS_PREFIX } from '../../common/constants'; - -type Props = { - currentMsgAggregator?: string; - columns: any[]; - mainEntity: EntityInfoType; - suggestions: any; - onSelect?: (value: string) => void; -}; - -const PAGE_SIZE = isMobile ? 3 : 5; - -const Suggestion: React.FC = ({ - currentMsgAggregator, - columns, - mainEntity, - suggestions, - onSelect, -}) => { - const [dimensions, setDimensions] = useState([]); - const [metrics, setMetrics] = useState([]); - const [dimensionIndex, setDimensionIndex] = useState(0); - const [metricIndex, setMetricIndex] = useState(0); - - const fields = columns - .filter(column => currentMsgAggregator !== 'tag' || column.showType !== 'NUMBER') - .concat(isMobile ? [] : mainEntity?.dimensions || []) - .map(item => item.name); - - useEffect(() => { - setDimensions( - suggestions.dimensions - .filter((dimension: any) => !fields.some(field => field === dimension.name)) - .map((item: any) => item.name) - ); - setMetrics( - suggestions.metrics - .filter((metric: any) => !fields.some(field => field === metric.name)) - .map((item: any) => item.name) - ); - }, []); - - const reloadDimensionCmds = () => { - const dimensionPageCount = Math.ceil(dimensions.length / PAGE_SIZE); - setDimensionIndex((dimensionIndex + 1) % dimensionPageCount); - }; - - const reloadMetricCmds = () => { - const metricPageCount = Math.ceil(metrics.length / PAGE_SIZE); - setMetricIndex((metricIndex + 1) % metricPageCount); - }; - - const dimensionList = dimensions.slice( - dimensionIndex * PAGE_SIZE, - (dimensionIndex + 1) * PAGE_SIZE - ); - - const metricList = metrics.slice(metricIndex * PAGE_SIZE, (metricIndex + 1) * PAGE_SIZE); - - if (!dimensionList.length && !metricList.length) { - return null; - } - - const prefixCls = `${CLS_PREFIX}-suggestion`; - - const suggestionClass = classNames(prefixCls, { - [`${prefixCls}-mobile`]: isMobile, - }); - - const sectionItemClass = classNames({ - [`${prefixCls}-section-item-selectable`]: onSelect !== undefined, - }); - - return ( -
- -
问答支持多轮对话,您可以继续输入:
- {metricList.length > 0 && ( -
-
指标:
-
- {metricList.map((metric, index) => { - let metricNode = ( -
{ - if (onSelect) { - onSelect(metric); - } - }} - > - {metric} -
- ); - return ( - <> - {metricNode} - {index < metricList.length - 1 && '、'} - - ); - })} -
- {metrics.length > PAGE_SIZE && ( -
{ - reloadMetricCmds(); - }} - > - - {!isMobile &&
换一批
} -
- )} -
- )} - {dimensionList.length > 0 && ( -
-
维度:
-
- {dimensionList.map((dimension, index) => { - return ( - <> -
{ - if (onSelect) { - onSelect(dimension); - } - }} - > - {dimension} -
- {index < dimensionList.length - 1 && '、'} - - ); - })} -
- {dimensions.length > PAGE_SIZE && ( -
{ - reloadDimensionCmds(); - }} - > - - {!isMobile &&
换一批
} -
- )} -
- )} -
-
- ); -}; - -export default Suggestion; diff --git a/webapp/packages/chat-sdk/src/components/Suggestion/style.less b/webapp/packages/chat-sdk/src/components/Suggestion/style.less deleted file mode 100644 index 7fe8d0ccd..000000000 --- a/webapp/packages/chat-sdk/src/components/Suggestion/style.less +++ /dev/null @@ -1,59 +0,0 @@ -@import '../../styles/index.less'; - -@suggestion-prefix-cls: ~'@{supersonic-chat-prefix}-suggestion'; - -.@{suggestion-prefix-cls} { - margin-top: 30px; - - .@{suggestion-prefix-cls}-mobile { - margin-top: 12px; - font-size: 13px; - } - - &-tip { - margin-bottom: 12px; - } - - &-content-section { - display: flex; - align-items: center; - margin-bottom: 10px; - row-gap: 12px; - } - - &-title { - color: var(--text-color-fourth); - } - - &-section-items { - display: flex; - flex-wrap: wrap; - align-items: center; - } - - &-section-item-selectable { - cursor: pointer; - &:hover { - color: var(--chat-blue); - } - } - - &-reload { - display: flex; - align-items: center; - margin-right: 14px; - margin-left: 20px; - color: var(--text-color-fourth); - font-size: 12px; - column-gap: 4px; - cursor: pointer; - - &:hover { - color: var(--chat-blue); - } - } - - &-reload-icon { - font-size: 10px; - } -} diff --git a/webapp/packages/chat-sdk/src/components/Tools/FeedbackModal.tsx b/webapp/packages/chat-sdk/src/components/Tools/FeedbackModal.tsx new file mode 100644 index 000000000..a1b546309 --- /dev/null +++ b/webapp/packages/chat-sdk/src/components/Tools/FeedbackModal.tsx @@ -0,0 +1,55 @@ +import { Modal, Input, message } from 'antd'; +import { CLS_PREFIX } from '../../common/constants'; +import { useState } from 'react'; + +const { TextArea } = Input; + +type Props = { + visible: boolean; + feedbackValue: string; + onSubmit: (feedback: string) => void; + onClose: () => void; +}; + +const FeedbackModal: React.FC = ({ visible, feedbackValue, onSubmit, onClose }) => { + const [feedback, setFeedback] = useState(feedbackValue); + const prefixCls = `${CLS_PREFIX}-tools`; + + const onOk = () => { + if (feedback.trim() === '') { + message.warning('请输入点评内容'); + return; + } + onSubmit(feedback); + }; + + const onFeedbackChange = (e: React.ChangeEvent) => { + setFeedback(e.target.value); + }; + + return ( + +
+
评价
+