mirror of
https://github.com/tencentmusic/supersonic.git
synced 2026-04-28 11:54:20 +08:00
first commit
This commit is contained in:
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
203
webapp/packages/supersonic-fe/src/pages/Chat/Conversation.tsx
Normal file
203
webapp/packages/supersonic-fe/src/pages/Chat/Conversation.tsx
Normal 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);
|
||||
@@ -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));
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
28
webapp/packages/supersonic-fe/src/pages/Chat/constants.ts
Normal file
28
webapp/packages/supersonic-fe/src/pages/Chat/constants.ts
Normal 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]: '维度值',
|
||||
};
|
||||
238
webapp/packages/supersonic-fe/src/pages/Chat/index.tsx
Normal file
238
webapp/packages/supersonic-fe/src/pages/Chat/index.tsx
Normal 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;
|
||||
22
webapp/packages/supersonic-fe/src/pages/Chat/service.ts
Normal file
22
webapp/packages/supersonic-fe/src/pages/Chat/service.ts
Normal 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`);
|
||||
}
|
||||
579
webapp/packages/supersonic-fe/src/pages/Chat/style.less
Normal file
579
webapp/packages/supersonic-fe/src/pages/Chat/style.less
Normal 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;
|
||||
}
|
||||
34
webapp/packages/supersonic-fe/src/pages/Chat/type.ts
Normal file
34
webapp/packages/supersonic-fe/src/pages/Chat/type.ts
Normal 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',
|
||||
}
|
||||
16
webapp/packages/supersonic-fe/src/pages/Chat/utils.ts
Normal file
16
webapp/packages/supersonic-fe/src/pages/Chat/utils.ts
Normal 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('、')}` : ''
|
||||
}`;
|
||||
}
|
||||
Reference in New Issue
Block a user