first commit

This commit is contained in:
jerryjzhang
2023-06-12 18:44:01 +08:00
commit dc4fc69b57
879 changed files with 573090 additions and 0 deletions

View File

@@ -0,0 +1,270 @@
import IconFont from '@/components/IconFont';
import { getTextWidth, groupByColumn, isMobile } from '@/utils/utils';
import { AutoComplete, Select, Tag } from 'antd';
import classNames from 'classnames';
import { debounce } from 'lodash';
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
import type { ForwardRefRenderFunction } from 'react';
import { searchRecommend } from 'supersonic-chat-sdk';
import { SemanticTypeEnum, SEMANTIC_TYPE_MAP } from '../constants';
import styles from './style.less';
import { PLACE_HOLDER } from '@/common/constants';
type Props = {
inputMsg: string;
chatId?: number;
onInputMsgChange: (value: string) => void;
onSendMsg: (msg: string, domainId?: number) => void;
};
const { OptGroup, Option } = Select;
let isPinyin = false;
let isSelect = false;
const compositionStartEvent = () => {
isPinyin = true;
};
const compositionEndEvent = () => {
isPinyin = false;
};
const ChatFooter: ForwardRefRenderFunction<any, Props> = (
{ inputMsg, chatId, onInputMsgChange, onSendMsg },
ref,
) => {
const [stepOptions, setStepOptions] = useState<Record<string, any[]>>({});
const [open, setOpen] = useState(false);
const [focused, setFocused] = useState(false);
const inputRef = useRef<any>();
const fetchRef = useRef(0);
const inputFocus = () => {
inputRef.current?.focus();
};
const inputBlur = () => {
inputRef.current?.blur();
};
useImperativeHandle(ref, () => ({
inputFocus,
inputBlur,
}));
const initEvents = () => {
const autoCompleteEl = document.getElementById('chatInput');
autoCompleteEl!.addEventListener('compositionstart', compositionStartEvent);
autoCompleteEl!.addEventListener('compositionend', compositionEndEvent);
};
const removeEvents = () => {
const autoCompleteEl = document.getElementById('chatInput');
if (autoCompleteEl) {
autoCompleteEl.removeEventListener('compositionstart', compositionStartEvent);
autoCompleteEl.removeEventListener('compositionend', compositionEndEvent);
}
};
useEffect(() => {
initEvents();
return () => {
removeEvents();
};
}, []);
const debounceGetWordsFunc = useCallback(() => {
const getAssociateWords = async (msg: string, chatId?: number) => {
if (isPinyin) {
return;
}
fetchRef.current += 1;
const fetchId = fetchRef.current;
const res = await searchRecommend(msg, chatId);
if (fetchId !== fetchRef.current) {
return;
}
const recommends = msg ? res.data.data || [] : [];
const stepOptionList = recommends.map((item: any) => item.subRecommend);
if (stepOptionList.length > 0 && stepOptionList.every((item: any) => item !== null)) {
const data = groupByColumn(recommends, 'domainName');
const optionsData =
isMobile && recommends.length > 6
? Object.keys(data)
.slice(0, 4)
.reduce((result, key) => {
result[key] = data[key].slice(
0,
Object.keys(data).length > 2 ? 2 : Object.keys(data).length > 1 ? 3 : 6,
);
return result;
}, {})
: data;
setStepOptions(optionsData);
} else {
setStepOptions({});
}
setOpen(recommends.length > 0);
};
return debounce(getAssociateWords, 20);
}, []);
const [debounceGetWords] = useState<any>(debounceGetWordsFunc);
useEffect(() => {
if (!isSelect) {
debounceGetWords(inputMsg, chatId);
} else {
isSelect = false;
}
if (!inputMsg) {
setStepOptions({});
}
}, [inputMsg]);
useEffect(() => {
if (!focused) {
setOpen(false);
}
}, [focused]);
useEffect(() => {
const autoCompleteDropdown = document.querySelector(
`.${styles.autoCompleteDropdown}`,
) as HTMLElement;
if (!autoCompleteDropdown) {
return;
}
const textWidth = getTextWidth(inputMsg);
if (Object.keys(stepOptions).length > 0) {
autoCompleteDropdown.style.marginLeft = `${textWidth}px`;
}
}, [stepOptions]);
const sendMsg = (value: string) => {
const option = Object.keys(stepOptions)
.reduce((result: any[], item) => {
result = result.concat(stepOptions[item]);
return result;
}, [])
.find((item) =>
Object.keys(stepOptions).length === 1
? item.recommend === value
: `${item.domainName || ''}${item.recommend}` === value,
);
if (option && isSelect) {
onSendMsg(option.recommend, option.domainId);
} else {
onSendMsg(value);
}
};
const autoCompleteDropdownClass = classNames(styles.autoCompleteDropdown, {
[styles.external]: true,
[styles.mobile]: isMobile,
});
const onSelect = (value: string) => {
isSelect = true;
sendMsg(value);
setOpen(false);
setTimeout(() => {
isSelect = false;
}, 200);
};
const chatFooterClass = classNames(styles.chatFooter, {
[styles.mobile]: isMobile,
});
return (
<div className={chatFooterClass}>
<div className={styles.composer}>
<div className={styles.composerInputWrapper}>
<AutoComplete
className={styles.composerInput}
placeholder={PLACE_HOLDER}
value={inputMsg}
onChange={onInputMsgChange}
onSelect={onSelect}
autoFocus={!isMobile}
backfill
ref={inputRef}
id="chatInput"
onKeyDown={(e) => {
if ((e.code === 'Enter' || e.code === 'NumpadEnter') && !isSelect) {
const chatInputEl: any = document.getElementById('chatInput');
sendMsg(chatInputEl.value);
setOpen(false);
}
}}
onFocus={() => {
setFocused(true);
}}
onBlur={() => {
setFocused(false);
}}
dropdownClassName={autoCompleteDropdownClass}
listHeight={500}
allowClear
open={open}
getPopupContainer={isMobile ? (triggerNode) => triggerNode.parentNode : undefined}
>
{Object.keys(stepOptions).map((key) => {
return (
<OptGroup key={key} label={key}>
{stepOptions[key].map((option) => (
<Option
key={`${option.recommend}${option.domainName ? `_${option.domainName}` : ''}`}
value={
Object.keys(stepOptions).length === 1
? option.recommend
: `${option.domainName || ''}${option.recommend}`
}
className={styles.searchOption}
>
<div className={styles.optionContent}>
{option.schemaElementType && (
<Tag
className={styles.semanticType}
color={
option.schemaElementType === SemanticTypeEnum.DIMENSION ||
option.schemaElementType === SemanticTypeEnum.DOMAIN
? 'blue'
: option.schemaElementType === SemanticTypeEnum.VALUE
? 'geekblue'
: 'orange'
}
>
{SEMANTIC_TYPE_MAP[option.schemaElementType] ||
option.schemaElementType ||
'维度'}
</Tag>
)}
{option.subRecommend}
</div>
</Option>
))}
</OptGroup>
);
})}
</AutoComplete>
<div
className={classNames(styles.sendBtn, {
[styles.sendBtnActive]: inputMsg?.length > 0,
})}
onClick={() => {
sendMsg(inputMsg);
}}
>
<IconFont type="icon-ios-send" />
</div>
</div>
</div>
</div>
);
};
export default forwardRef(ChatFooter);

View File

@@ -0,0 +1,153 @@
.chatFooter {
position: relative;
z-index: 10;
display: flex;
flex-direction: column;
margin-top: 6px;
margin-right: 20px;
margin-bottom: 40px;
.composer {
display: flex;
height: 46px;
.composerInputWrapper {
flex: 1;
.composerInput {
width: 100%;
height: 100%;
:global {
.ant-select-selector {
box-sizing: border-box;
height: 100%;
overflow: hidden;
color: rgba(0, 0, 0, 0.87);
font-size: 16px;
word-break: break-all;
background: #fff;
border: 0;
border-radius: 24px;
box-shadow: rgba(0, 0, 0, 0.07) 0 -0.5px 0, rgba(0, 0, 0, 0.1) 0 0 18px;
transition: border-color 0.15s ease-in-out;
resize: none;
.ant-select-selection-search-input {
height: 100% !important;
padding: 0 20px;
}
.ant-select-selection-search {
right: 0 !important;
left: 0 !important;
}
.ant-select-selection-placeholder {
padding-left: 10px !important;
line-height: 45px;
}
}
.ant-select-clear {
right: auto;
left: 500px;
width: 16px;
height: 16px;
margin-top: -8px;
font-size: 16px;
}
}
}
:global {
.ant-select-focused {
.ant-select-selector {
box-shadow: rgb(74, 114, 245) 0 0 3px !important;
}
}
}
}
}
.sendBtn {
position: absolute;
top: 50%;
right: 6px;
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
color: #fff;
font-size: 20px;
background-color: rgb(184, 184, 191);
border: unset;
border-radius: 50%;
transform: translateY(-50%);
transition: background-color 0.3s ease 0s;
&.sendBtnActive {
background-color: var(--chat-blue);
}
}
&.mobile {
height: 40px;
margin: 12px;
margin-bottom: 20px;
.composer {
height: 40px;
:global {
.ant-select-selector {
font-size: 14px !important;
}
.ant-select-selection-placeholder {
line-height: 39px !important;
}
}
}
}
}
.searchOption {
padding: 6px 20px;
color: #212121;
font-size: 16px;
}
.mobile {
.searchOption {
min-height: 26px;
padding: 2px 12px;
font-size: 14px;
}
}
.domain {
margin-top: 2px;
color: var(--text-color-fourth);
font-size: 13px;
line-height: 12px;
}
.autoCompleteDropdown {
left: 285px !important;
width: fit-content !important;
min-width: 50px !important;
border-radius: 6px;
&.external {
left: 226px !important;
}
&.mobile {
left: 20px !important;
}
}
.semanticType {
margin-right: 10px;
}

View File

@@ -0,0 +1,203 @@
import IconFont from '@/components/IconFont';
import { Dropdown, Menu, message } from 'antd';
import classNames from 'classnames';
import {
useEffect,
useState,
forwardRef,
ForwardRefRenderFunction,
useImperativeHandle,
} from 'react';
import { useLocation } from 'umi';
import ConversationHistory from './components/ConversationHistory';
import ConversationModal from './components/ConversationModal';
import { deleteConversation, getAllConversations, saveConversation } from './service';
import styles from './style.less';
import { ConversationDetailType } from './type';
type Props = {
currentConversation?: ConversationDetailType;
onSelectConversation: (conversation: ConversationDetailType, name?: string) => void;
};
const Conversation: ForwardRefRenderFunction<any, Props> = (
{ currentConversation, onSelectConversation },
ref,
) => {
const location = useLocation();
const { q, cid } = (location as any).query;
const [originConversations, setOriginConversations] = useState<ConversationDetailType[]>([]);
const [conversations, setConversations] = useState<ConversationDetailType[]>([]);
const [editModalVisible, setEditModalVisible] = useState(false);
const [editConversation, setEditConversation] = useState<ConversationDetailType>();
const [historyVisible, setHistoryVisible] = useState(false);
useImperativeHandle(ref, () => ({
updateData,
onAddConversation,
}));
const updateData = async () => {
const { data } = await getAllConversations();
const conversationList = (data || []).slice(0, 5);
setOriginConversations(data || []);
setConversations(conversationList);
return conversationList;
};
const initData = async () => {
const data = await updateData();
if (data.length > 0) {
const chatId = localStorage.getItem('CONVERSATION_ID') || cid;
if (chatId) {
const conversation = data.find((item: any) => item.chatId === +chatId);
if (conversation) {
onSelectConversation(conversation);
} else {
onSelectConversation(data[0]);
}
} else {
onSelectConversation(data[0]);
}
} else {
onAddConversation();
}
};
useEffect(() => {
if (q && cid === undefined) {
onAddConversation(q);
} else {
initData();
}
}, [q]);
const addConversation = async (name?: string) => {
await saveConversation(name || '新问答对话');
return updateData();
};
const onDeleteConversation = async (id: number) => {
await deleteConversation(id);
initData();
};
const onAddConversation = async (name?: string) => {
const data = await addConversation(name);
onSelectConversation(data[0], name);
};
const onOperate = (key: string, conversation: ConversationDetailType) => {
if (key === 'editName') {
setEditConversation(conversation);
setEditModalVisible(true);
} else if (key === 'delete') {
onDeleteConversation(conversation.chatId);
}
};
const onNewChat = () => {
onAddConversation('新问答对话');
};
const onShowHistory = () => {
setHistoryVisible(true);
};
const onShare = () => {
message.info('正在开发中,敬请期待');
};
return (
<div className={styles.conversation}>
<div className={styles.leftSection}>
<div className={styles.conversationList}>
{conversations.map((item) => {
const conversationItemClass = classNames(styles.conversationItem, {
[styles.activeConversationItem]: currentConversation?.chatId === item.chatId,
});
return (
<Dropdown
key={item.chatId}
overlay={
<Menu
items={[
{ label: '修改对话名称', key: 'editName' },
{ label: '删除', key: 'delete' },
]}
onClick={({ key }) => {
onOperate(key, item);
}}
/>
}
trigger={['contextMenu']}
>
<div
key={item.chatId}
className={conversationItemClass}
onClick={() => {
onSelectConversation(item);
}}
>
<div className={styles.conversationItemContent}>
<IconFont type="icon-chat1" className={styles.conversationIcon} />
<div className={styles.conversationContent} title={item.chatName}>
{item.chatName}
</div>
</div>
</div>
</Dropdown>
);
})}
<div className={styles.conversationItem} onClick={onShowHistory}>
<div className={styles.conversationItemContent}>
<IconFont
type="icon-more2"
className={`${styles.conversationIcon} ${styles.historyIcon}`}
/>
<div className={styles.conversationContent}></div>
</div>
</div>
</div>
<div className={styles.operateSection}>
<div className={styles.operateItem} onClick={onNewChat}>
<IconFont type="icon-add" className={`${styles.operateIcon} ${styles.addIcon}`} />
<div className={styles.operateLabel}></div>
</div>
<div className={styles.operateItem} onClick={onShare}>
<IconFont
type="icon-fenxiang2"
className={`${styles.operateIcon} ${styles.shareIcon}`}
/>
<div className={styles.operateLabel}></div>
</div>
</div>
</div>
{historyVisible && (
<ConversationHistory
conversations={originConversations}
onSelectConversation={(conversation) => {
onSelectConversation(conversation);
setHistoryVisible(false);
}}
onClose={() => {
setHistoryVisible(false);
}}
/>
)}
<ConversationModal
visible={editModalVisible}
editConversation={editConversation}
onClose={() => {
setEditModalVisible(false);
}}
onFinish={() => {
setEditModalVisible(false);
updateData();
}}
/>
</div>
);
};
export default forwardRef(Conversation);

View File

@@ -0,0 +1,89 @@
import Text from './components/Text';
import { memo, useCallback, useEffect } from 'react';
import { isEqual } from 'lodash';
import styles from './style.less';
import { connect, Dispatch } from 'umi';
import { ChatItem } from 'supersonic-chat-sdk';
import type { MsgDataType } from 'supersonic-chat-sdk';
import { MessageItem, MessageTypeEnum } from './type';
type Props = {
id: string;
chatId: number;
messageList: MessageItem[];
dispatch: Dispatch;
onClickMessageContainer: () => void;
onMsgDataLoaded: (data: MsgDataType) => void;
onSelectSuggestion: (value: string) => void;
onUpdateMessageScroll: () => void;
};
const MessageContainer: React.FC<Props> = ({
id,
chatId,
messageList,
dispatch,
onClickMessageContainer,
onMsgDataLoaded,
onSelectSuggestion,
onUpdateMessageScroll,
}) => {
const onWindowResize = useCallback(() => {
dispatch({
type: 'windowResize/setTriggerResize',
payload: true,
});
setTimeout(() => {
dispatch({
type: 'windowResize/setTriggerResize',
payload: false,
});
}, 0);
}, []);
useEffect(() => {
window.addEventListener('resize', onWindowResize);
return () => {
window.removeEventListener('resize', onWindowResize);
};
}, []);
return (
<div id={id} className={styles.messageContainer} onClick={onClickMessageContainer}>
<div className={styles.messageList}>
{messageList.map((msgItem: MessageItem, index: number) => {
return (
<div key={`${msgItem.id}`} id={`${msgItem.id}`} className={styles.messageItem}>
{msgItem.type === MessageTypeEnum.TEXT && <Text position="left" data={msgItem.msg} />}
{msgItem.type === MessageTypeEnum.QUESTION && (
<>
<Text position="right" data={msgItem.msg} quote={msgItem.quote} />
<ChatItem
msg={msgItem.msg || ''}
msgData={msgItem.msgData}
conversationId={chatId}
classId={msgItem.domainId}
isLastMessage={index === messageList.length - 1}
onLastMsgDataLoaded={onMsgDataLoaded}
onSelectSuggestion={onSelectSuggestion}
onUpdateMessageScroll={onUpdateMessageScroll}
suggestionEnable
/>
</>
)}
</div>
);
})}
</div>
</div>
);
};
function areEqual(prevProps: Props, nextProps: Props) {
if (prevProps.id === nextProps.id && isEqual(prevProps.messageList, nextProps.messageList)) {
return true;
}
return false;
}
export default connect()(memo(MessageContainer, areEqual));

View File

@@ -0,0 +1,56 @@
import moment from 'moment';
import styles from './style.less';
import type { ChatContextType } from 'supersonic-chat-sdk';
type Props = {
chatContext: ChatContextType;
};
const Context: React.FC<Props> = ({ chatContext }) => {
const { domainName, metrics, dateInfo, filters } = chatContext;
return (
<div className={styles.context}>
<div className={styles.title}></div>
<div className={styles.content}>
<div className={styles.field}>
<span className={styles.fieldName}></span>
<span className={styles.fieldValue}>{domainName}</span>
</div>
{dateInfo && (
<div className={styles.field}>
<span className={styles.fieldName}></span>
<span className={styles.fieldValue}>
{dateInfo.text ||
`${moment(dateInfo.endDate).diff(moment(dateInfo.startDate), 'days') + 1}`}
</span>
</div>
)}
{metrics && metrics.length > 0 && (
<div className={styles.field}>
<span className={styles.fieldName}></span>
<span className={styles.fieldValue}>
{metrics.map((metric) => metric.name).join('、')}
</span>
</div>
)}
{filters && filters.length > 0 && (
<div className={styles.filterSection}>
<div className={styles.fieldName}></div>
<div className={styles.filterValues}>
{filters.map((filter) => {
return (
<div className={styles.filterItem} key={filter.name}>
{filter.name}{filter.value}
</div>
);
})}
</div>
</div>
)}
</div>
</div>
);
};
export default Context;

View File

@@ -0,0 +1,70 @@
.context {
display: flex;
flex-direction: column;
.title {
margin-bottom: 22px;
color: var(--text-color);
font-size: 16px;
line-height: 24px;
}
.desc {
max-height: 350px;
margin-top: 10px;
margin-bottom: 10px;
overflow-y: auto;
color: var(--text-color-third);
font-size: 13px;
line-height: 22px;
}
.field {
display: flex;
flex-wrap: wrap;
align-items: center;
margin-bottom: 10px;
&.columnLayout {
flex-direction: column;
}
.avatar {
margin-right: 8px;
}
}
.filterSection {
margin-bottom: 12px;
}
.fieldName {
margin-right: 6px;
color: var(--text-color);
font-weight: 500;
}
.fieldValue {
color: var(--text-color);
&.switchField {
cursor: pointer;
}
}
.filterValues {
display: flex;
flex-wrap: wrap;
margin-top: 10px;
font-size: 13px;
column-gap: 4px;
row-gap: 4px;
.filterItem {
padding: 2px 12px;
color: var(--text-color-secondary);
background-color: var(--body-background);
border-radius: 13px;
}
}
}

View File

@@ -0,0 +1,41 @@
import { getFormattedValueData } from '@/utils/utils';
import moment from 'moment';
import React from 'react';
import styles from './style.less';
import type { EntityInfoType, MsgDataType } from 'supersonic-chat-sdk';
type Props = {
currentEntity: MsgDataType;
};
const Introduction: React.FC<Props> = ({ currentEntity }) => {
const { entityInfo } = currentEntity;
const { dimensions, metrics } = entityInfo || ({} as EntityInfoType);
return (
<div className={styles.introduction}>
{dimensions
?.filter((dimension) => !dimension.bizName.includes('photo'))
.map((dimension) => {
return (
<div className={styles.field} key={dimension.name}>
<span className={styles.fieldName}>{dimension.name}</span>
<span className={styles.fieldValue}>
{dimension.bizName.includes('publish_time')
? moment(dimension.value).format('YYYY-MM-DD')
: dimension.value}
</span>
</div>
);
})}
{metrics?.map((metric) => (
<div className={styles.field} key={metric.name}>
<span className={styles.fieldName}>{metric.name}</span>
<span className={styles.fieldValue}>{getFormattedValueData(metric.value)}</span>
</div>
))}
</div>
);
};
export default Introduction;

View File

@@ -0,0 +1,63 @@
.introduction {
display: flex;
flex-direction: column;
padding-bottom: 4px;
.title {
margin-bottom: 22px;
color: var(--text-color);
font-size: 16px;
line-height: 24px;
}
.desc {
max-height: 350px;
margin-top: 10px;
margin-bottom: 10px;
overflow-y: auto;
color: var(--text-color-third);
font-size: 13px;
line-height: 22px;
}
.field {
display: flex;
flex-wrap: wrap;
align-items: center;
margin-bottom: 10px;
&.columnLayout {
flex-direction: column;
}
.avatar {
margin-right: 8px;
}
}
.fieldName {
margin-right: 6px;
color: var(--text-color);
font-weight: 500;
}
.fieldValue {
color: var(--text-color);
&.switchField {
cursor: pointer;
}
&.dimensionFieldValue {
max-width: 90px;
// white-space: nowrap;
}
&.mainNameFieldValue {
max-width: 90px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}

View File

@@ -0,0 +1,28 @@
import classNames from 'classnames';
import Context from './Context';
import Introduction from './Introduction';
import styles from './style.less';
import type { MsgDataType } from 'supersonic-chat-sdk';
type Props = {
currentEntity?: MsgDataType;
};
const RightSection: React.FC<Props> = ({ currentEntity }) => {
const rightSectionClass = classNames(styles.rightSection, {
[styles.external]: true,
});
return (
<div className={rightSectionClass}>
{currentEntity && (
<div className={styles.entityInfo}>
{currentEntity?.chatContext && <Context chatContext={currentEntity.chatContext} />}
<Introduction currentEntity={currentEntity} />
</div>
)}
</div>
);
};
export default RightSection;

View File

@@ -0,0 +1,19 @@
.rightSection {
width: 225px;
height: calc(100vh - 48px);
padding-right: 10px;
padding-bottom: 10px;
padding-left: 20px;
overflow-y: auto;
.entityInfo {
margin-top: 30px;
.topInfo {
margin-bottom: 20px;
color: var(--text-color-third);
font-weight: 500;
font-size: 14px;
}
}
}

View File

@@ -0,0 +1,43 @@
import { CloseOutlined } from '@ant-design/icons';
import moment from 'moment';
import type { ConversationDetailType } from '../../type';
import styles from './style.less';
type Props = {
conversations: ConversationDetailType[];
onSelectConversation: (conversation: ConversationDetailType) => void;
onClose: () => void;
};
const ConversationHistory: React.FC<Props> = ({ conversations, onSelectConversation, onClose }) => {
return (
<div className={styles.conversationHistory}>
<div className={styles.header}>
<div className={styles.headerTitle}></div>
<CloseOutlined className={styles.headerClose} onClick={onClose} />
</div>
<div className={styles.conversationContent}>
{conversations.slice(0, 1000).map((conversation) => {
return (
<div
key={conversation.chatId}
className={styles.conversationItem}
onClick={() => {
onSelectConversation(conversation);
}}
>
<div className={styles.conversationName} title={conversation.chatName}>
{conversation.chatName}
</div>
<div className={styles.conversationTime}>
{moment(conversation.lastTime).format('YYYY-MM-DD')}
</div>
</div>
);
})}
</div>
</div>
);
};
export default ConversationHistory;

View File

@@ -0,0 +1,64 @@
.conversationHistory {
position: absolute;
top: 0;
left: 0;
z-index: 10;
display: flex;
flex-direction: column;
width: 215px;
height: calc(100vh - 48px);
overflow: hidden;
background: #f3f3f7;
border-right: 1px solid var(--border-color-base);
.header {
display: flex;
align-items: center;
justify-content: space-between;
height: 50px;
padding: 0 16px;
border-bottom: 1px solid #e8e8e8;
.headerTitle {
color: var(--text-color);
font-weight: 500;
font-size: 16px;
}
.headerClose {
color: var(--text-color-third);
font-size: 16px;
cursor: pointer;
&:hover {
color: var(--chat-blue);
}
}
}
.conversationContent {
flex: 1;
overflow: auto;
.conversationItem {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 8px 16px;
border-bottom: 1px solid var(--border-color-base-bg-5);
cursor: pointer;
row-gap: 2px;
&:hover {
background: var(--light-blue-background);
}
.conversationName {
width: 170px;
overflow: hidden;
color: var(--text-color);
font-size: 14px;
white-space: nowrap;
text-overflow: ellipsis;
}
.conversationTime {
color: var(--text-color-third);
font-size: 13px;
}
}
}
}

View File

@@ -0,0 +1,65 @@
import { Form, Input, Modal } from 'antd';
import { useEffect, useRef, useState } from 'react';
import { updateConversationName } from '../../service';
import type { ConversationDetailType } from '../../type';
const FormItem = Form.Item;
type Props = {
visible: boolean;
editConversation?: ConversationDetailType;
onClose: () => void;
onFinish: (conversationName: string) => void;
};
const layout = {
labelCol: { span: 6 },
wrapperCol: { span: 18 },
};
const ConversationModal: React.FC<Props> = ({ visible, editConversation, onClose, onFinish }) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const conversationNameInputRef = useRef<any>();
useEffect(() => {
if (visible) {
form.setFieldsValue({ conversationName: editConversation!.chatName });
setTimeout(() => {
conversationNameInputRef.current.focus({
cursor: 'all',
});
}, 0);
}
}, [visible]);
const onConfirm = async () => {
const values = await form.validateFields();
setLoading(true);
await updateConversationName(values.conversationName, editConversation!.chatId);
setLoading(false);
onFinish(values.conversationName);
};
return (
<Modal
title="修改问答对话名称"
visible={visible}
onCancel={onClose}
onOk={onConfirm}
confirmLoading={loading}
>
<Form {...layout} form={form}>
<FormItem name="conversationName" label="名称" rules={[{ required: true }]}>
<Input
placeholder="请输入问答对话名称"
ref={conversationNameInputRef}
onPressEnter={onConfirm}
/>
</FormItem>
</Form>
</Modal>
);
};
export default ConversationModal;

View File

@@ -0,0 +1,35 @@
import classNames from 'classnames';
import styles from './style.less';
type Props = {
position: 'left' | 'right';
bubbleClassName?: string;
aggregator?: string;
noTime?: boolean;
};
const Message: React.FC<Props> = ({ position, children, bubbleClassName }) => {
const messageClass = classNames(styles.message, {
[styles.left]: position === 'left',
[styles.right]: position === 'right',
});
return (
<div className={messageClass}>
<div className={styles.messageContent}>
<div className={styles.messageBody}>
<div
className={`${styles.bubble}${bubbleClassName ? ` ${bubbleClassName}` : ''}`}
onClick={(e) => {
e.stopPropagation();
}}
>
{children}
</div>
</div>
</div>
</div>
);
};
export default Message;

View File

@@ -0,0 +1,19 @@
import Message from './Message';
import styles from './style.less';
type Props = {
position: 'left' | 'right';
data: any;
quote?: string;
};
const Text: React.FC<Props> = ({ position, data, quote }) => {
return (
<Message position={position} bubbleClassName={styles.textBubble}>
{position === 'right' && quote && <div className={styles.quote}>{quote}</div>}
<div className={styles.text}>{data}</div>
</Message>
);
};
export default Text;

View File

@@ -0,0 +1,19 @@
import { CHAT_BLUE } from '@/common/constants';
import { Spin } from 'antd';
import BeatLoader from 'react-spinners/BeatLoader';
import Message from './Message';
import styles from './style.less';
const Typing = () => {
return (
<Message position="left" bubbleClassName={styles.typingBubble}>
<Spin
spinning={true}
indicator={<BeatLoader color={CHAT_BLUE} size={10} />}
className={styles.typing}
/>
</Message>
);
};
export default Typing;

View File

@@ -0,0 +1,277 @@
.message {
.messageContent {
display: flex;
align-items: flex-start;
.messageBody {
width: 100%;
}
.avatar {
margin-right: 4px;
}
.bubble {
box-sizing: border-box;
min-width: 1px;
max-width: 100%;
padding: 8px 16px 10px;
background: rgba(255, 255, 255, 0.8);
border: 1px solid transparent;
border-radius: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.14), 0 0 2px rgba(0, 0, 0, 0.12);
.text {
line-height: 1.5;
white-space: pre-wrap;
overflow-wrap: break-word;
user-select: text;
}
.textMsg {
padding: 12px 0 5px;
}
.topBar {
display: flex;
align-items: center;
max-width: 100%;
padding: 4px 0 8px;
overflow-x: auto;
color: var(--text-color);
font-weight: 500;
font-size: 14px;
white-space: nowrap;
border-bottom: 1px solid rgba(0, 0, 0, 0.03);
.messageTitleWrapper {
display: flex;
align-items: center;
}
.messageTitle {
display: flex;
align-items: center;
color: var(--text-color);
font-weight: 500;
font-size: 14px;
white-space: nowrap;
}
}
}
}
&.right {
.messageContent {
flex-direction: row-reverse;
.bubble {
float: right;
box-sizing: border-box;
padding: 8px 16px;
color: #fff;
font-size: 16px;
background: linear-gradient(81.62deg, #2870ea 8.72%, var(--chat-blue) 85.01%);
border: 1px solid transparent;
border-radius: 12px 4px 12px 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.14), 0 0 2px rgba(0, 0, 0, 0.12);
.text {
&::selection {
background: #1ba1f7;
}
}
}
}
}
}
.textBubble {
width: fit-content;
}
.listenerSex {
padding-bottom: 24px;
}
.listenerArea {
padding-top: 24px;
padding-bottom: 12px;
}
.typing {
width: 100%;
padding: 0 5px;
:global {
.ant-spin-dot {
width: 100%;
}
}
}
.messageEntityName {
cursor: pointer;
&:hover {
color: var(--primary-color);
}
}
.messageAvatar {
margin-right: 8px;
}
.dataHolder {
position: relative;
}
.subTitle {
margin-left: 20px;
color: var(--text-color-third);
font-weight: normal;
font-size: 12px;
.subTitleValue {
margin-left: 6px;
color: var(--text-color);
font-size: 13px;
}
}
.avatarPopover {
:global {
.ant-popover-inner-content {
padding: 3px 4px !important;
}
}
}
.moreOption {
display: flex;
align-items: center;
margin-top: 10px;
color: var(--text-color-fourth);
font-size: 12px;
.selectOthers {
color: var(--text-color);
cursor: pointer;
&:hover {
color: var(--primary-color);
}
}
.indicators {
display: flex;
align-items: center;
margin-left: 12px;
column-gap: 12px;
.indicator {
cursor: pointer;
&:hover {
color: var(--primary-color);
}
}
}
}
.contentName {
max-width: 350px;
white-space: nowrap;
text-overflow: ellipsis;
}
.aggregatorIndicator {
color: var(--text-color);
font-weight: 500;
font-size: 20px;
}
.entityId {
display: flex;
align-items: center;
margin-left: 12px;
column-gap: 4px;
.idTitle {
color: var(--text-color-fourth);
font-size: 12px;
}
.idValue {
color: var(--text-color-fourth);
font-size: 13px;
cursor: pointer;
&:hover {
color: var(--primary-color);
}
}
}
.typingBubble {
width: fit-content;
}
.quote {
margin-bottom: 4px;
padding: 0 4px 0 6px;
color: var(--border-color-base);
font-size: 13px;
border-left: 4px solid var(--border-color-base);
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
}
.filterSection {
display: flex;
align-items: center;
color: var(--text-color-secondary);
font-weight: normal;
font-size: 13px;
.filterItem {
padding: 2px 12px;
color: var(--text-color-secondary);
background-color: #edf2f2;
border-radius: 13px;
}
}
.noPermissionTip {
display: flex;
align-items: center;
}
.tip {
margin-left: 6px;
color: var(--text-color-third);
}
.infoBar {
display: flex;
flex-wrap: wrap;
align-items: center;
margin-top: 20px;
column-gap: 20px;
}
.mainEntityInfo {
display: flex;
flex-wrap: wrap;
align-items: center;
font-size: 13px;
column-gap: 20px;
.infoItem {
display: flex;
align-items: center;
.infoName {
color: var(--text-color-fourth);
}
.infoValue {
color: var(--text-color-secondary);
}
}
}

View File

@@ -0,0 +1,28 @@
export const THEME_COLOR_LIST = [
'#3369FF',
'#36D2B8',
'#DB8D76',
'#47B359',
'#8545E6',
'#E0B18B',
'#7258F3',
'#0095FF',
'#52CC8F',
'#6675FF',
'#CC516E',
'#5CA9E6',
];
export enum SemanticTypeEnum {
DOMAIN = 'DOMAIN',
DIMENSION = 'DIMENSION',
METRIC = 'METRIC',
VALUE = 'VALUE',
}
export const SEMANTIC_TYPE_MAP = {
[SemanticTypeEnum.DOMAIN]: '主题域',
[SemanticTypeEnum.DIMENSION]: '维度',
[SemanticTypeEnum.METRIC]: '指标',
[SemanticTypeEnum.VALUE]: '维度值',
};

View File

@@ -0,0 +1,238 @@
import { updateMessageContainerScroll, isMobile, uuid } from '@/utils/utils';
import { useEffect, useRef, useState } from 'react';
import { Helmet } from 'umi';
import MessageContainer from './MessageContainer';
import styles from './style.less';
import { ConversationDetailType, MessageItem, MessageTypeEnum } from './type';
import { updateConversationName } from './service';
import { useThrottleFn } from 'ahooks';
import Conversation from './Conversation';
import RightSection from './RightSection';
import ChatFooter from './ChatFooter';
import classNames from 'classnames';
import { DEFAULT_CONVERSATION_NAME, WEB_TITLE } from '@/common/constants';
import { HistoryMsgItemType, MsgDataType, getHistoryMsg, queryContext } from 'supersonic-chat-sdk';
import { getConversationContext } from './utils';
const Chat = () => {
const [messageList, setMessageList] = useState<MessageItem[]>([]);
const [inputMsg, setInputMsg] = useState('');
const [pageNo, setPageNo] = useState(1);
const [hasNextPage, setHasNextPage] = useState(false);
const [historyInited, setHistoryInited] = useState(false);
const [currentConversation, setCurrentConversation] = useState<
ConversationDetailType | undefined
>(isMobile ? { chatId: 0, chatName: '问答对话' } : undefined);
const [currentEntity, setCurrentEntity] = useState<MsgDataType>();
const conversationRef = useRef<any>();
const chatFooterRef = useRef<any>();
const sendHelloRsp = () => {
setMessageList([
{
id: uuid(),
type: MessageTypeEnum.TEXT,
msg: '您好,请问有什么我能帮您吗?',
},
]);
};
const updateHistoryMsg = async (page: number) => {
const res = await getHistoryMsg(page, currentConversation!.chatId);
const { hasNextPage, list } = res.data.data;
setMessageList([
...list.map((item: HistoryMsgItemType) => ({
id: item.questionId,
type: MessageTypeEnum.QUESTION,
msg: item.queryText,
msgData: item.queryResponse,
})),
...(page === 1 ? [] : messageList),
]);
setHasNextPage(hasNextPage);
if (page === 1) {
if (list.length === 0) {
sendHelloRsp();
} else {
setCurrentEntity(list[list.length - 1].queryResponse);
}
updateMessageContainerScroll();
setHistoryInited(true);
}
if (page > 1) {
const msgEle = document.getElementById(`${messageList[0]?.id}`);
msgEle?.scrollIntoView();
}
};
const { run: handleScroll } = useThrottleFn(
(e) => {
if (e.target.scrollTop === 0 && hasNextPage) {
updateHistoryMsg(pageNo + 1);
setPageNo(pageNo + 1);
}
},
{
leading: true,
trailing: true,
wait: 200,
},
);
useEffect(() => {
if (historyInited) {
const messageContainerEle = document.getElementById('messageContainer');
messageContainerEle?.addEventListener('scroll', handleScroll);
}
return () => {
const messageContainerEle = document.getElementById('messageContainer');
messageContainerEle?.removeEventListener('scroll', handleScroll);
};
}, [historyInited]);
const inputFocus = () => {
if (!isMobile) {
chatFooterRef.current?.inputFocus();
}
};
const inputBlur = () => {
chatFooterRef.current?.inputBlur();
};
useEffect(() => {
if (!currentConversation) {
return;
}
setCurrentEntity(undefined);
const { initMsg, domainId } = currentConversation;
if (initMsg) {
inputFocus();
if (initMsg === DEFAULT_CONVERSATION_NAME) {
sendHelloRsp();
return;
}
onSendMsg(currentConversation.initMsg, [], domainId, true);
return;
}
updateHistoryMsg(1);
setPageNo(1);
}, [currentConversation]);
const modifyConversationName = async (name: string) => {
await updateConversationName(name, currentConversation!.chatId);
conversationRef?.current?.updateData();
window.history.replaceState('', '', `?q=${name}&cid=${currentConversation!.chatId}`);
};
const onSendMsg = async (
msg?: string,
list?: MessageItem[],
domainId?: number,
firstMsg?: boolean,
) => {
const currentMsg = msg || inputMsg;
if (currentMsg.trim() === '') {
setInputMsg('');
return;
}
let quote = '';
if (currentEntity && !firstMsg) {
const { data } = await queryContext(currentMsg, currentConversation!.chatId);
if (data.code === 200 && data.data.domainId === currentEntity.chatContext?.domainId) {
quote = getConversationContext(data.data);
}
}
setMessageList([
...(list || messageList),
{ id: uuid(), msg: currentMsg, domainId, type: MessageTypeEnum.QUESTION, quote },
]);
updateMessageContainerScroll();
setInputMsg('');
modifyConversationName(currentMsg);
};
const onInputMsgChange = (value: string) => {
const inputMsgValue = value || '';
setInputMsg(inputMsgValue);
};
const saveConversationToLocal = (conversation: ConversationDetailType) => {
if (conversation) {
if (conversation.chatId !== -1) {
localStorage.setItem('CONVERSATION_ID', `${conversation.chatId}`);
}
} else {
localStorage.removeItem('CONVERSATION_ID');
}
};
const onSelectConversation = (conversation: ConversationDetailType, name?: string) => {
window.history.replaceState('', '', `?q=${conversation.chatName}&cid=${conversation.chatId}`);
setCurrentConversation({
...conversation,
initMsg: name,
});
saveConversationToLocal(conversation);
};
const onMsgDataLoaded = (data: MsgDataType) => {
setCurrentEntity(data);
updateMessageContainerScroll();
};
const chatClass = classNames(styles.chat, {
[styles.external]: true,
[styles.mobile]: isMobile,
});
return (
<div className={chatClass}>
<Helmet title={WEB_TITLE} />
<div className={styles.topSection} />
<div className={styles.chatSection}>
{!isMobile && (
<Conversation
currentConversation={currentConversation}
onSelectConversation={onSelectConversation}
ref={conversationRef}
/>
)}
<div className={styles.chatApp}>
{currentConversation && (
<div className={styles.chatBody}>
<div className={styles.chatContent}>
<MessageContainer
id="messageContainer"
messageList={messageList}
chatId={currentConversation?.chatId}
onClickMessageContainer={() => {
inputFocus();
}}
onMsgDataLoaded={onMsgDataLoaded}
onSelectSuggestion={onSendMsg}
onUpdateMessageScroll={updateMessageContainerScroll}
/>
<ChatFooter
inputMsg={inputMsg}
chatId={currentConversation?.chatId}
onInputMsgChange={onInputMsgChange}
onSendMsg={(msg: string, domainId?: number) => {
onSendMsg(msg, messageList, domainId);
if (isMobile) {
inputBlur();
}
}}
ref={chatFooterRef}
/>
</div>
</div>
)}
</div>
{!isMobile && <RightSection currentEntity={currentEntity} />}
</div>
</div>
);
};
export default Chat;

View File

@@ -0,0 +1,22 @@
import { request } from 'umi';
const prefix = '/api';
export function saveConversation(chatName: string) {
return request<Result<any>>(`${prefix}/chat/manage/save?chatName=${chatName}`, { method: 'POST' });
}
export function updateConversationName(chatName: string, chatId: number = 0) {
return request<Result<any>>(
`${prefix}/chat/manage/updateChatName?chatName=${chatName}&chatId=${chatId}`,
{ method: 'POST' },
);
}
export function deleteConversation(chatId: number) {
return request<Result<any>>(`${prefix}/chat/manage/delete?chatId=${chatId}`, { method: 'POST' });
}
export function getAllConversations() {
return request<Result<any>>(`${prefix}/chat/manage/getAll`);
}

View File

@@ -0,0 +1,579 @@
.chat {
height: calc(100vh - 48px) !important;
overflow-y: hidden;
background: linear-gradient(180deg, rgba(23, 74, 228, 0) 29.44%, rgba(23, 74, 228, 0.06) 100%),
linear-gradient(90deg, #f3f3f7 0%, #f3f3f7 20%, #ebf0f9 60%, #f3f3f7 80%, #f3f3f7 100%);
&.external {
.chatApp {
width: calc(100vw - 450px) !important;
height: calc(100vh - 58px) !important;
}
}
&.mobile {
height: 100vh !important;
.chatSection {
// height: 100vh !important;
height: 100% !important;
}
.conversation {
// height: 100vh !important;
height: 100% !important;
}
.chatApp {
width: 100vw !important;
// height: 100vh !important;
height: 100% !important;
}
}
}
.chatSection {
display: flex;
height: calc(100vh - 48px) !important;
overflow-y: hidden;
}
.chatBody {
height: 100%;
}
.conversation {
position: relative;
width: 225px;
height: calc(100vh - 48px);
.leftSection {
width: 100%;
height: 100%;
}
}
.chatApp {
display: flex;
flex-direction: column;
width: calc(100vw - 510px);
height: calc(100vh - 58px) !important;
margin-top: 10px;
color: rgba(0, 0, 0, 0.87);
.emptyHolder {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.navBar {
position: relative;
z-index: 10;
display: flex;
align-items: center;
height: 40px;
padding: 0 10px;
background: rgb(243 243 243);
border-bottom: 1px solid rgb(228, 228, 228);
.conversationNameWrapper {
display: flex;
align-items: center;
.conversationName {
padding: 4px 12px;
color: var(--text-color-third) !important;
font-size: 14px !important;
border-radius: 4px;
cursor: pointer;
.editIcon {
margin-left: 10px;
color: var(--text-color-fourth);
font-size: 14px;
}
&:hover {
background-color: rgba(0, 0, 0, 0.03);
}
}
.divider {
width: 1px;
height: 16px;
margin-right: 4px;
margin-left: 12px;
background-color: var(--text-color-fourth);
}
}
.conversationInput {
width: 300px;
color: var(--text-color-third) !important;
font-size: 14px !important;
cursor: default !important;
}
}
.chatBody {
display: flex;
flex: 1;
.chatContent {
display: flex;
flex-direction: column;
width: 100%;
.messageContainer {
position: relative;
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
overflow-x: hidden;
overflow-y: scroll;
.messageList {
display: flex;
flex-direction: column;
padding: 0 20px 90px 4px;
row-gap: 20px;
.messageItem {
display: flex;
flex-direction: column;
row-gap: 20px;
}
&.reportLoading {
position: absolute;
bottom: 10000px;
width: 100%;
}
}
}
}
}
}
.mobile {
.messageList {
padding: 0 12px 20px !important;
}
}
.keyword {
color: var(--primary-color);
}
.messageItem {
margin-top: 12px;
}
.messageTime {
display: flex;
align-items: center;
justify-content: center;
margin-top: 20px;
color: #999;
}
.modules {
display: flex;
align-items: center;
height: 40px;
padding: 8px 12px;
overflow: hidden;
background: rgb(243, 243, 243);
border-top: 1px solid rgb(228, 228, 228);
.moduleType {
width: 80px;
margin-right: 12px;
:global {
.ant-select-selection-item {
font-size: 13px !important;
}
.ant-select-selection-item,
.ant-select-single:not(.ant-select-customize-input) .ant-select-selector::after {
line-height: 28px !important;
}
}
.moduleSelect {
box-sizing: border-box;
width: 100%;
height: 100%;
color: rgba(0, 0, 0, 0.87);
word-break: break-all;
border: 0;
border-radius: 20px;
:global {
.ant-select-selector {
height: 30px !important;
border: 1px solid var(--primary-color) !important;
border-radius: 20px !important;
}
.ant-select-arrow {
margin-top: -4px !important;
}
}
}
}
.example {
margin-right: 4px;
// margin-left: 16px;
color: var(--text-color-secondary);
font-size: 13px;
}
:global {
button[ant-click-animating-without-extra-node]::after {
border: 0 none;
opacity: 0;
animation: none 0 ease 0 1 normal;
}
.iconBtn {
color: rgba(0, 0, 0, 0.4) !important;
background: transparent !important;
border: 0 !important;
box-shadow: none !important;
.scrollerControlIcon {
font-size: 12px;
}
&:hover {
background: rgba(0, 0, 0, 0.05) !important;
}
}
}
.modulesInner {
display: flex;
flex: 1;
overflow-x: scroll;
overflow-y: hidden;
scroll-behavior: smooth;
&::-webkit-scrollbar {
display: none;
}
.moduleItem {
position: relative;
display: flex;
align-items: center;
margin-left: 8px;
padding: 4px 11px;
color: var(--text-color);
font-weight: 500;
font-size: 14px;
line-height: 1.43;
white-space: nowrap;
background: #fff;
border: 1px solid #fff;
border-radius: 20px;
cursor: pointer;
transition: 0.15s ease-in-out;
&:hover {
background: rgba(0, 0, 0, 0.05);
background-clip: padding-box;
border-color: rgba(0, 0, 0, 0.05);
}
&:first-child {
margin-left: 0 !important;
}
&.activeModuleItem {
color: var(--primary-color) !important;
border-color: var(--primary-color) !important;
}
&.cmdItem {
font-weight: normal;
font-size: 13px;
}
}
}
}
.optGroupBar {
display: flex;
align-items: center;
justify-content: space-between;
// width: 530px;
&.recentSearchBar {
padding-top: 2px;
padding-bottom: 5px;
}
.optGroupTitle {
color: #333;
font-weight: 500;
font-size: 14px;
}
.recentSearch {
color: #999;
font-weight: normal;
}
.clearSearch {
color: #666;
font-size: 14px;
cursor: pointer;
}
}
.recentSearchOption {
padding-left: 12px !important;
.optionItem {
display: flex;
align-items: center;
justify-content: space-between;
width: 483px;
.removeRecentMsg {
display: none;
cursor: pointer;
}
}
&:hover {
.removeRecentMsg {
display: block;
}
}
}
.conversationList {
padding-top: 20px;
.conversationItem {
padding-left: 16px;
cursor: pointer;
.conversationItemContent {
display: flex;
align-items: center;
padding: 12px 0;
color: var(--text-color-third);
.conversationIcon {
margin-right: 10px;
color: var(--text-color-fourth);
font-size: 20px;
}
.conversationContent {
width: 160px;
overflow: hidden;
color: var(--text-color-third);
white-space: nowrap;
text-overflow: ellipsis;
}
}
&.activeConversationItem,
&:hover {
.conversationContent {
color: var(--chat-blue);
}
}
}
}
.addConversation {
display: flex;
align-items: center;
margin-left: 12px;
color: var(--text-color-third);
column-gap: 4px;
cursor: pointer;
&:hover {
color: var(--primary-color);
}
}
.loadingWords {
padding: 40px 1px;
}
.associateWordsOption {
display: flex;
align-items: center;
column-gap: 10px;
.optionContent {
display: flex;
align-items: center;
min-width: 450px;
column-gap: 10px;
}
.indicatorItem {
min-width: 180px;
.indicatorLabel {
color: var(--text-color-fourth);
font-size: 12px;
}
.indicatorValue {
margin-left: 4px;
font-size: 13px;
}
}
}
.collapseBtn {
margin: 0 10px;
color: var(--text-color-third);
font-size: 16px;
cursor: pointer;
&:hover {
color: var(--primary-color);
}
}
.autoCompleteDropdown {
width: 650px !important;
min-width: 650px !important;
border-radius: 10px;
:global {
.ant-select-item {
min-height: 36px !important;
line-height: 26px !important;
&:not(:first-child):hover {
background: #f5f5f5 !important;
}
}
// .ant-select-item-option-active:not(.ant-select-item-option-disabled) {
// background-color: #fff;
// }
}
}
.recommendItemTitle {
margin-right: 14px;
padding: 4px 12px;
background-color: var(--deep-background);
border-radius: 2px;
}
.refeshQuestions {
cursor: pointer;
.reloadIcon {
margin-right: 4px;
}
}
.recommendQuestions {
display: flex;
align-items: center;
justify-content: center;
width: 54px;
height: 100%;
color: var(--text-color-fourth);
font-size: 18px;
background: rgba(255, 255, 255, 0.2);
cursor: pointer;
&:hover {
background: rgba(255, 255, 255, 0.1);
}
}
.currentTool {
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 0 24px 0 2px;
color: var(--chat-blue);
font-weight: 500;
font-size: 16px;
background: rgba(255, 255, 255, 0.2);
.removeTool {
position: absolute;
top: 14px;
right: 6px;
color: var(--text-color-fifth);
font-size: 14px;
cursor: pointer;
&:hover {
color: var(--chat-blue);
}
}
}
.associateOption {
display: flex;
align-items: center;
}
.associateOptionAvatar {
width: 32px;
margin-right: 10px;
}
.optionContent {
min-width: 330px;
}
.optionIndicator {
min-width: 120px;
margin-left: 4px;
color: var(--text-color-fourth);
font-size: 12px;
}
.operateSection {
margin-top: 20px;
padding-left: 15px;
}
.operateItem {
display: flex;
align-items: center;
padding: 10px 0;
cursor: pointer;
.operateIcon {
margin-right: 10px;
color: var(--text-color-fourth);
font-size: 20px;
}
.operateLabel {
color: var(--text-color-third);
font-size: 14px;
}
&:hover {
.operateLabel {
color: var(--chat-blue);
}
}
}
.messageLoading {
margin-top: 30px;
}

View File

@@ -0,0 +1,34 @@
import { MsgDataType } from 'supersonic-chat-sdk';
export enum MessageTypeEnum {
TEXT = 'text', // 指标文本
QUESTION = 'question',
TAG = 'tag', // 标签
SUGGESTION = 'suggestion', // 建议
NO_PERMISSION = 'no_permission', // 无权限
SEMANTIC_DETAIL = 'semantic_detail', // 语义指标/维度等信息详情
}
export type MessageItem = {
id: string | number;
type?: MessageTypeEnum;
msg?: string;
domainId?: number;
msgData?: MsgDataType;
quote?: string;
};
export type ConversationDetailType = {
chatId: number;
chatName: string;
createTime?: string;
creator?: string;
lastQuestion?: string;
lastTime?: string;
initMsg?: string;
domainId?: number;
};
export enum MessageModeEnum {
INTERPRET = 'interpret',
}

View File

@@ -0,0 +1,16 @@
import { ChatContextType } from 'supersonic-chat-sdk';
import moment from 'moment';
export function getConversationContext(chatContext: ChatContextType) {
if (!chatContext) return '';
const { domainName, metrics, dateInfo } = chatContext;
// const dimensionStr =
// dimensions?.length > 0 ? dimensions.map((dimension) => dimension.name).join('、') : '';
const timeStr =
dateInfo?.text ||
`${moment(dateInfo?.endDate).diff(moment(dateInfo?.startDate), 'days') + 1}`;
return `${domainName}${
metrics?.length > 0 ? `${timeStr}${metrics.map((metric) => metric.name).join('、')}` : ''
}`;
}