mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-20 06:34:55 +00:00
add drill down dimensions and metric period compare and modify layout (#22)
* [feature](webapp) add drill down dimensions and metric period compare and modify layout * [feature](webapp) add drill down dimensions and metric period compare and modify layout --------- Co-authored-by: williamhliu <williamhliu@tencent.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import { SemanticTypeEnum, SEMANTIC_TYPE_MAP } from '../constants';
|
||||
import styles from './style.less';
|
||||
import { PLACE_HOLDER } from '../constants';
|
||||
import { DomainType } from '../type';
|
||||
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';
|
||||
|
||||
type Props = {
|
||||
inputMsg: string;
|
||||
@@ -17,6 +18,8 @@ type Props = {
|
||||
currentDomain?: DomainType;
|
||||
domains: DomainType[];
|
||||
isMobileMode?: boolean;
|
||||
collapsed: boolean;
|
||||
onToggleCollapseBtn: () => void;
|
||||
onInputMsgChange: (value: string) => void;
|
||||
onSendMsg: (msg: string, domainId?: number) => void;
|
||||
onAddConversation: () => void;
|
||||
@@ -41,6 +44,8 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
||||
currentDomain,
|
||||
domains,
|
||||
isMobileMode,
|
||||
collapsed,
|
||||
onToggleCollapseBtn,
|
||||
onInputMsgChange,
|
||||
onSendMsg,
|
||||
onAddConversation,
|
||||
@@ -239,6 +244,9 @@ const ChatFooter: ForwardRefRenderFunction<any, Props> = (
|
||||
return (
|
||||
<div className={chatFooterClass}>
|
||||
<div className={styles.composer}>
|
||||
<div className={styles.collapseBtn} onClick={onToggleCollapseBtn}>
|
||||
{collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
</div>
|
||||
<Tooltip title="新建对话">
|
||||
<IconFont
|
||||
type="icon-icon-add-conversation-line"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Form, Input, Modal } from 'antd';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { updateConversationName } from '../../../service';
|
||||
import type { ConversationDetailType } from '../../../type';
|
||||
import { CHAT_TITLE } from '../../../constants';
|
||||
import { updateConversationName } from '../../service';
|
||||
import type { ConversationDetailType } from '../../type';
|
||||
import { CHAT_TITLE } from '../../constants';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
import IconFont from '@/components/IconFont';
|
||||
import { Dropdown, Input, Menu } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
useEffect,
|
||||
useState,
|
||||
forwardRef,
|
||||
ForwardRefRenderFunction,
|
||||
useImperativeHandle,
|
||||
} from 'react';
|
||||
import { useLocation } from 'umi';
|
||||
import ConversationModal from './ConversationModal';
|
||||
import { deleteConversation, getAllConversations, saveConversation } from '../service';
|
||||
import styles from './style.less';
|
||||
import { ConversationDetailType } from '../type';
|
||||
import moment from 'moment';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { DEFAULT_CONVERSATION_NAME } from '@/common/constants';
|
||||
|
||||
type Props = {
|
||||
currentConversation?: ConversationDetailType;
|
||||
collapsed?: boolean;
|
||||
onSelectConversation: (
|
||||
conversation: ConversationDetailType,
|
||||
name?: string,
|
||||
domainId?: number,
|
||||
entityId?: string,
|
||||
) => void;
|
||||
};
|
||||
|
||||
const Conversation: ForwardRefRenderFunction<any, Props> = (
|
||||
{ currentConversation, collapsed, onSelectConversation },
|
||||
ref,
|
||||
) => {
|
||||
const location = useLocation();
|
||||
const { q, cid, domainId, entityId } = (location as any).query;
|
||||
const [conversations, setConversations] = useState<ConversationDetailType[]>([]);
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
const [editConversation, setEditConversation] = useState<ConversationDetailType>();
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
updateData,
|
||||
onAddConversation,
|
||||
}));
|
||||
|
||||
const updateData = async () => {
|
||||
const { data } = await getAllConversations();
|
||||
const conversationList = 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 && window.location.href.includes('/workbench/chat')) {
|
||||
onAddConversation({ name: q, domainId: domainId ? +domainId : undefined, entityId });
|
||||
} else {
|
||||
initData();
|
||||
}
|
||||
}, [q]);
|
||||
|
||||
const addConversation = async (name?: string) => {
|
||||
await saveConversation(name || DEFAULT_CONVERSATION_NAME);
|
||||
return updateData();
|
||||
};
|
||||
|
||||
const onDeleteConversation = async (id: number) => {
|
||||
await deleteConversation(id);
|
||||
initData();
|
||||
};
|
||||
|
||||
const onAddConversation = async ({
|
||||
name,
|
||||
domainId,
|
||||
entityId,
|
||||
type,
|
||||
}: {
|
||||
name?: string;
|
||||
domainId?: number;
|
||||
entityId?: string;
|
||||
type?: string;
|
||||
} = {}) => {
|
||||
const data = await addConversation(name);
|
||||
onSelectConversation(data[0], type || name, domainId, entityId);
|
||||
};
|
||||
|
||||
const onOperate = (key: string, conversation: ConversationDetailType) => {
|
||||
if (key === 'editName') {
|
||||
setEditConversation(conversation);
|
||||
setEditModalVisible(true);
|
||||
} else if (key === 'delete') {
|
||||
onDeleteConversation(conversation.chatId);
|
||||
}
|
||||
};
|
||||
|
||||
const conversationClass = classNames(styles.conversation, {
|
||||
[styles.collapsed]: collapsed,
|
||||
});
|
||||
|
||||
const convertTime = (date: string) => {
|
||||
moment.locale('zh-cn');
|
||||
const now = moment();
|
||||
const inputDate = moment(date);
|
||||
const diffMinutes = now.diff(inputDate, 'minutes');
|
||||
if (diffMinutes < 1) {
|
||||
return '刚刚';
|
||||
} else if (inputDate.isSame(now, 'day')) {
|
||||
return inputDate.format('HH:mm');
|
||||
} else if (inputDate.isSame(now.subtract(1, 'day'), 'day')) {
|
||||
return '昨天';
|
||||
}
|
||||
return inputDate.format('MM/DD');
|
||||
};
|
||||
|
||||
const onSearchValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchValue(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={conversationClass}>
|
||||
<div className={styles.leftSection}>
|
||||
<div className={styles.searchConversation}>
|
||||
<Input
|
||||
placeholder="搜索"
|
||||
prefix={<SearchOutlined className={styles.searchIcon} />}
|
||||
className={styles.searchTask}
|
||||
value={searchValue}
|
||||
onChange={onSearchValueChange}
|
||||
allowClear
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.conversationList}>
|
||||
{conversations
|
||||
.filter(
|
||||
(conversation) =>
|
||||
searchValue === '' ||
|
||||
conversation.chatName.toLowerCase().includes(searchValue.toLowerCase()),
|
||||
)
|
||||
.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
|
||||
className={conversationItemClass}
|
||||
onClick={() => {
|
||||
onSelectConversation(item);
|
||||
}}
|
||||
>
|
||||
<IconFont type="icon-chat1" className={styles.conversationIcon} />
|
||||
<div className={styles.conversationContent}>
|
||||
<div className={styles.topTitleBar}>
|
||||
<div className={styles.conversationName}>{item.chatName}</div>
|
||||
<div className={styles.conversationTime}>
|
||||
{convertTime(item.lastTime || '')}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.subTitle}>{item.lastQuestion}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<ConversationModal
|
||||
visible={editModalVisible}
|
||||
editConversation={editConversation}
|
||||
onClose={() => {
|
||||
setEditModalVisible(false);
|
||||
}}
|
||||
onFinish={() => {
|
||||
setEditModalVisible(false);
|
||||
updateData();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(Conversation);
|
||||
@@ -0,0 +1,149 @@
|
||||
.conversation {
|
||||
position: relative;
|
||||
width: 260px;
|
||||
height: 100vh !important;
|
||||
background-color: #fff;
|
||||
border-right: 1px solid var(--border-color-base);
|
||||
|
||||
.leftSection {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.searchConversation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 9px 10px;
|
||||
|
||||
.searchIcon {
|
||||
color: #999 !important;
|
||||
}
|
||||
|
||||
.searchTask {
|
||||
font-size: 13px;
|
||||
background-color: #f5f5f5;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
box-shadow: none !important;
|
||||
|
||||
:global {
|
||||
.ant-input {
|
||||
font-size: 13px !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.conversationList {
|
||||
height: calc(100vh - 50px);
|
||||
padding: 2px 8px 0;
|
||||
overflow-y: auto;
|
||||
.conversationItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 2px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
.conversationIcon {
|
||||
margin-right: 10px;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.conversationContent {
|
||||
width: 100%;
|
||||
|
||||
.topTitleBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
.conversationName {
|
||||
width: 150px;
|
||||
margin-right: 2px;
|
||||
overflow: hidden;
|
||||
color: var(--text-color);
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.conversationTime {
|
||||
color: var(--text-color-six);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.subTitle {
|
||||
width: 180px;
|
||||
overflow: hidden;
|
||||
color: var(--text-color-six);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
&.activeConversationItem {
|
||||
background-color: var(--light-blue-background);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--light-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
width: 0;
|
||||
padding: 0;
|
||||
border-right: 0;
|
||||
|
||||
.leftSection {
|
||||
.searchConversation {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.conversationList {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.operateSection {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,33 +4,30 @@ import { isEqual } from 'lodash';
|
||||
import { ChatItem } from 'supersonic-chat-sdk';
|
||||
import type { MsgDataType } from 'supersonic-chat-sdk';
|
||||
import { MessageItem, MessageTypeEnum } from './type';
|
||||
import classNames from 'classnames';
|
||||
import { Skeleton } from 'antd';
|
||||
import styles from './style.less';
|
||||
import RecommendQuestions from './components/RecommendQuestions';
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
chatId: number;
|
||||
messageList: MessageItem[];
|
||||
miniProgramLoading: boolean;
|
||||
isMobileMode?: boolean;
|
||||
conversationCollapsed: boolean;
|
||||
onClickMessageContainer: () => void;
|
||||
onMsgDataLoaded: (data: MsgDataType, questionId: string | number) => void;
|
||||
onSelectSuggestion: (value: string) => void;
|
||||
onCheckMore: (data: MsgDataType) => void;
|
||||
onUpdateMessageScroll: () => void;
|
||||
};
|
||||
|
||||
const MessageContainer: React.FC<Props> = ({
|
||||
id,
|
||||
chatId,
|
||||
messageList,
|
||||
miniProgramLoading,
|
||||
isMobileMode,
|
||||
conversationCollapsed,
|
||||
onClickMessageContainer,
|
||||
onMsgDataLoaded,
|
||||
onSelectSuggestion,
|
||||
onUpdateMessageScroll,
|
||||
}) => {
|
||||
const [triggerResize, setTriggerResize] = useState(false);
|
||||
|
||||
@@ -41,6 +38,10 @@ const MessageContainer: React.FC<Props> = ({
|
||||
}, 0);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
onResize();
|
||||
}, [conversationCollapsed]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', onResize);
|
||||
return () => {
|
||||
@@ -48,10 +49,6 @@ const MessageContainer: React.FC<Props> = ({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const messageListClass = classNames(styles.messageList, {
|
||||
[styles.miniProgramLoading]: miniProgramLoading,
|
||||
});
|
||||
|
||||
const getFollowQuestions = (index: number) => {
|
||||
const followQuestions: string[] = [];
|
||||
const currentMsg = messageList[index];
|
||||
@@ -82,8 +79,7 @@ const MessageContainer: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div id={id} className={styles.messageContainer} onClick={onClickMessageContainer}>
|
||||
{miniProgramLoading && <Skeleton className={styles.messageLoading} paragraph={{ rows: 5 }} />}
|
||||
<div className={messageListClass}>
|
||||
<div className={styles.messageList}>
|
||||
{messageList.map((msgItem: MessageItem, index: number) => {
|
||||
const { id: msgId, domainId, type, msg, msgValue, identityMsg, msgData } = msgItem;
|
||||
|
||||
@@ -91,6 +87,9 @@ const MessageContainer: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div key={msgId} id={`${msgId}`} className={styles.messageItem}>
|
||||
{type === MessageTypeEnum.RECOMMEND_QUESTIONS && (
|
||||
<RecommendQuestions onSelectQuestion={onSelectSuggestion} />
|
||||
)}
|
||||
{type === MessageTypeEnum.TEXT && <Text position="left" data={msg} />}
|
||||
{type === MessageTypeEnum.QUESTION && (
|
||||
<>
|
||||
@@ -108,8 +107,6 @@ const MessageContainer: React.FC<Props> = ({
|
||||
onMsgDataLoaded={(data: MsgDataType) => {
|
||||
onMsgDataLoaded(data, msgId);
|
||||
}}
|
||||
onSelectSuggestion={onSelectSuggestion}
|
||||
onUpdateMessageScroll={onUpdateMessageScroll}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -125,7 +122,7 @@ function areEqual(prevProps: Props, nextProps: Props) {
|
||||
if (
|
||||
prevProps.id === nextProps.id &&
|
||||
isEqual(prevProps.messageList, nextProps.messageList) &&
|
||||
prevProps.miniProgramLoading === nextProps.miniProgramLoading
|
||||
prevProps.conversationCollapsed === nextProps.conversationCollapsed
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
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;
|
||||
@@ -1,64 +0,0 @@
|
||||
.conversationHistory {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: calc(100vh - 78px);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
import IconFont from '@/components/IconFont';
|
||||
import { Dropdown, Menu } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
useEffect,
|
||||
useState,
|
||||
forwardRef,
|
||||
ForwardRefRenderFunction,
|
||||
useImperativeHandle,
|
||||
} from 'react';
|
||||
import { useLocation } from 'umi';
|
||||
import ConversationHistory from './ConversationHistory';
|
||||
import ConversationModal from './ConversationModal';
|
||||
import { deleteConversation, getAllConversations, saveConversation } from '../../service';
|
||||
import styles from './style.less';
|
||||
import { ConversationDetailType } from '../../type';
|
||||
import { DEFAULT_CONVERSATION_NAME } from '../../constants';
|
||||
|
||||
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 && location.pathname === '/workbench/chat') {
|
||||
onAddConversation(q);
|
||||
} else {
|
||||
initData();
|
||||
}
|
||||
}, [q]);
|
||||
|
||||
const addConversation = async (name?: string) => {
|
||||
await saveConversation(name || DEFAULT_CONVERSATION_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 onShowHistory = () => {
|
||||
setHistoryVisible(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.conversation}>
|
||||
<div className={styles.conversationSection}>
|
||||
<div className={styles.sectionTitle}>对话管理</div>
|
||||
<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
|
||||
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>
|
||||
{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);
|
||||
@@ -1,50 +0,0 @@
|
||||
.conversation {
|
||||
position: relative;
|
||||
margin-top: 30px;
|
||||
padding: 0 10px;
|
||||
|
||||
.conversationSection {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.sectionTitle {
|
||||
margin-bottom: 12px;
|
||||
color: var(--text-color);
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.conversationList {
|
||||
.conversationItem {
|
||||
cursor: pointer;
|
||||
.conversationItemContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import type { MsgDataType } from 'supersonic-chat-sdk';
|
||||
import Domains from './Domains';
|
||||
import { ConversationDetailType, DomainType } from '../type';
|
||||
import DomainInfo from './Context/DomainInfo';
|
||||
import Conversation from './Conversation';
|
||||
import Conversation from '../Conversation';
|
||||
|
||||
type Props = {
|
||||
domains: DomainType[];
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import LeftAvatar from '../LeftAvatar';
|
||||
import Message from '../Message';
|
||||
import styles from './style.less';
|
||||
import { queryRecommendQuestions } from '../../service';
|
||||
import Typing from '../Typing';
|
||||
|
||||
type Props = {
|
||||
onSelectQuestion: (value: string) => void;
|
||||
};
|
||||
|
||||
const RecommendQuestions: React.FC<Props> = ({ onSelectQuestion }) => {
|
||||
const [questions, setQuestions] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const initData = async () => {
|
||||
setLoading(true);
|
||||
const res = await queryRecommendQuestions();
|
||||
setLoading(false);
|
||||
setQuestions(
|
||||
res.data?.reduce((result: any[], item: any) => {
|
||||
result = [
|
||||
...result,
|
||||
...item.recommendedQuestions.slice(0, 20).map((item: any) => item.question),
|
||||
];
|
||||
return result;
|
||||
}, []) || [],
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.recommendQuestions}>
|
||||
<LeftAvatar />
|
||||
{loading ? (
|
||||
<Typing />
|
||||
) : questions.length > 0 ? (
|
||||
<Message position="left" bubbleClassName={styles.recommendQuestionsMsg}>
|
||||
<div className={styles.title}>推荐问题:</div>
|
||||
<div className={styles.content}>
|
||||
{questions.map((question, index) => (
|
||||
<div
|
||||
key={`${question}_${index}`}
|
||||
className={styles.question}
|
||||
onClick={() => {
|
||||
onSelectQuestion(question);
|
||||
}}
|
||||
>
|
||||
{question}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Message>
|
||||
) : (
|
||||
<Message position="left">您好,请问有什么我可以帮您吗?</Message>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecommendQuestions;
|
||||
@@ -0,0 +1,35 @@
|
||||
.recommendQuestions {
|
||||
display: flex;
|
||||
|
||||
.recommendQuestionsMsg {
|
||||
padding: 12px 20px 20px !important;
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 16px;
|
||||
row-gap: 20px;
|
||||
|
||||
.question {
|
||||
padding: 0 6px;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
font-size: 12px;
|
||||
color: var(--text-color);
|
||||
border-radius:11px;
|
||||
background-color: #f4f4f4;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { updateMessageContainerScroll } from '@/utils/utils';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { querySuggestion } from '../../service';
|
||||
import { SuggestionType } from '../../type';
|
||||
import Message from '../Message';
|
||||
import styles from './style.less';
|
||||
|
||||
type Props = {
|
||||
domainId: number;
|
||||
onSelectSuggestion: (value: string) => void;
|
||||
};
|
||||
|
||||
const Suggestion: React.FC<Props> = ({ domainId, onSelectSuggestion }) => {
|
||||
const [data, setData] = useState<SuggestionType>({ dimensions: [], metrics: [] });
|
||||
const { metrics } = data;
|
||||
|
||||
const initData = async () => {
|
||||
const res = await querySuggestion(domainId);
|
||||
setData({
|
||||
dimensions: res.data.dimensions.slice(0, 5),
|
||||
metrics: res.data.metrics.slice(0, 5),
|
||||
});
|
||||
updateMessageContainerScroll();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.suggestion}>
|
||||
<Message position="left" bubbleClassName={styles.suggestionMsg}>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.rowTitle}>您可能还想问以下指标:</div>
|
||||
<div className={styles.rowContent}>
|
||||
{metrics.map((metric, index) => {
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
className={styles.contentItemName}
|
||||
onClick={() => {
|
||||
onSelectSuggestion(metric.name);
|
||||
}}
|
||||
>
|
||||
{metric.name}
|
||||
</span>
|
||||
{index !== metrics.length - 1 && <span>、</span>}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Message>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Suggestion;
|
||||
@@ -0,0 +1,37 @@
|
||||
.suggestion {
|
||||
margin-left: 46px;
|
||||
|
||||
.suggestionMsg {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px !important;
|
||||
row-gap: 12px;
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
column-gap: 4px;
|
||||
row-gap: 12px;
|
||||
|
||||
.rowTitle {
|
||||
color: var(--text-color-third);
|
||||
}
|
||||
|
||||
.rowContent {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
color: var(--text-color);
|
||||
row-gap: 12px;
|
||||
|
||||
.contentItemName {
|
||||
color: var(--chat-blue);
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid var(--chat-blue);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,6 +229,7 @@
|
||||
|
||||
.typingBubble {
|
||||
width: fit-content;
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
.quote {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { HistoryMsgItemType, MsgDataType, getHistoryMsg } from 'supersonic-chat-
|
||||
import 'supersonic-chat-sdk/dist/index.css';
|
||||
import { setToken as setChatSdkToken } from 'supersonic-chat-sdk';
|
||||
import { TOKEN_KEY } from '@/services/request';
|
||||
import Conversation from './Conversation';
|
||||
|
||||
type Props = {
|
||||
isCopilotMode?: boolean;
|
||||
@@ -35,6 +36,7 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
||||
const [miniProgramLoading, setMiniProgramLoading] = useState(false);
|
||||
const [domains, setDomains] = useState<DomainType[]>([]);
|
||||
const [currentDomain, setCurrentDomain] = useState<DomainType>();
|
||||
const [conversationCollapsed, setConversationCollapsed] = useState(false);
|
||||
const conversationRef = useRef<any>();
|
||||
const chatFooterRef = useRef<any>();
|
||||
|
||||
@@ -42,14 +44,14 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
||||
setMessageList([
|
||||
{
|
||||
id: uuid(),
|
||||
type: MessageTypeEnum.TEXT,
|
||||
msg: '您好,请问有什么我能帮您吗?',
|
||||
type: MessageTypeEnum.RECOMMEND_QUESTIONS,
|
||||
// msg: '您好,请问有什么我能帮您吗?',
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const existInstuctionMsg = (list: HistoryMsgItemType[]) => {
|
||||
return list.some((msg) => msg.queryResponse.queryMode === MessageTypeEnum.INSTRUCTION);
|
||||
return list.some((msg) => msg.queryResult?.queryMode === MessageTypeEnum.INSTRUCTION);
|
||||
};
|
||||
|
||||
const updateScroll = (list: HistoryMsgItemType[]) => {
|
||||
@@ -71,11 +73,11 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
||||
...list.map((item: HistoryMsgItemType) => ({
|
||||
id: item.questionId,
|
||||
type:
|
||||
item.queryResponse?.queryMode === MessageTypeEnum.INSTRUCTION
|
||||
item.queryResult?.queryMode === MessageTypeEnum.INSTRUCTION
|
||||
? MessageTypeEnum.INSTRUCTION
|
||||
: MessageTypeEnum.QUESTION,
|
||||
msg: item.queryText,
|
||||
msgData: item.queryResponse,
|
||||
msgData: item.queryResult,
|
||||
isHistory: true,
|
||||
})),
|
||||
...(page === 1 ? [] : messageList),
|
||||
@@ -85,7 +87,7 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
||||
if (list.length === 0) {
|
||||
sendHelloRsp();
|
||||
} else {
|
||||
setCurrentEntity(list[list.length - 1].queryResponse);
|
||||
setCurrentEntity(list[list.length - 1].queryResult);
|
||||
}
|
||||
updateScroll(list);
|
||||
setHistoryInited(true);
|
||||
@@ -203,6 +205,10 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
||||
modifyConversationName(currentMsg);
|
||||
};
|
||||
|
||||
const onToggleCollapseBtn = () => {
|
||||
setConversationCollapsed(!conversationCollapsed);
|
||||
};
|
||||
|
||||
const onInputMsgChange = (value: string) => {
|
||||
const inputMsgValue = value || '';
|
||||
setInputMsg(inputMsgValue);
|
||||
@@ -295,6 +301,7 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
||||
const chatClass = classNames(styles.chat, {
|
||||
[styles.mobile]: isMobileMode,
|
||||
[styles.copilot]: isCopilotMode,
|
||||
[styles.conversationCollapsed]: conversationCollapsed,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -302,6 +309,12 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
||||
{!isMobileMode && <Helmet title={WEB_TITLE} />}
|
||||
<div className={styles.topSection} />
|
||||
<div className={styles.chatSection}>
|
||||
<Conversation
|
||||
currentConversation={currentConversation}
|
||||
collapsed={conversationCollapsed}
|
||||
onSelectConversation={onSelectConversation}
|
||||
ref={conversationRef}
|
||||
/>
|
||||
<div className={styles.chatApp}>
|
||||
{currentConversation && (
|
||||
<div className={styles.chatBody}>
|
||||
@@ -310,22 +323,23 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
||||
id="messageContainer"
|
||||
messageList={messageList}
|
||||
chatId={currentConversation?.chatId}
|
||||
miniProgramLoading={miniProgramLoading}
|
||||
isMobileMode={isMobileMode}
|
||||
conversationCollapsed={conversationCollapsed}
|
||||
onClickMessageContainer={() => {
|
||||
inputFocus();
|
||||
}}
|
||||
onMsgDataLoaded={onMsgDataLoaded}
|
||||
onSelectSuggestion={onSendMsg}
|
||||
onCheckMore={onCheckMore}
|
||||
onUpdateMessageScroll={updateMessageContainerScroll}
|
||||
/>
|
||||
<ChatFooter
|
||||
inputMsg={inputMsg}
|
||||
chatId={currentConversation?.chatId}
|
||||
domains={domains}
|
||||
currentDomain={currentDomain}
|
||||
collapsed={conversationCollapsed}
|
||||
isMobileMode={isMobileMode}
|
||||
onToggleCollapseBtn={onToggleCollapseBtn}
|
||||
onInputMsgChange={onInputMsgChange}
|
||||
onSendMsg={(msg: string, domainId?: number) => {
|
||||
onSendMsg(msg, messageList, domainId);
|
||||
@@ -343,7 +357,7 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isMobileMode && (
|
||||
{/* {!isMobileMode && (
|
||||
<RightSection
|
||||
domains={domains}
|
||||
currentEntity={currentEntity}
|
||||
@@ -353,7 +367,7 @@ const Chat: React.FC<Props> = ({ isCopilotMode }) => {
|
||||
onSelectConversation={onSelectConversation}
|
||||
conversationRef={conversationRef}
|
||||
/>
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -30,3 +30,24 @@ export function getDomainList() {
|
||||
skipErrorHandler: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function updateQAFeedback(questionId: number, score: number) {
|
||||
return request<Result<any>>(
|
||||
`${prefix}/chat/manage/updateQAFeedback?id=${questionId}&score=${score}&feedback=`,
|
||||
{
|
||||
method: 'POST',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function querySuggestion(domainId: number) {
|
||||
return request<Result<any>>(`${prefix}/chat/recommend/${domainId}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
export function queryRecommendQuestions() {
|
||||
return request<Result<any>>(`${prefix}/chat/recommend/question`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
.chatApp {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: calc(100vw - 225px);
|
||||
width: calc(100vw - 260px);
|
||||
height: calc(100vh - 48px);
|
||||
padding-left: 20px;
|
||||
padding-left: 10px;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
|
||||
.emptyHolder {
|
||||
@@ -230,6 +230,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.conversationCollapsed {
|
||||
.chatApp {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
height: 100% !important;
|
||||
|
||||
@@ -243,7 +249,7 @@
|
||||
}
|
||||
|
||||
.chatApp {
|
||||
width: calc(100% - 225px) !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ export enum MessageTypeEnum {
|
||||
NO_PERMISSION = 'no_permission', // 无权限
|
||||
SEMANTIC_DETAIL = 'semantic_detail', // 语义指标/维度等信息详情
|
||||
INSTRUCTION = 'INSTRUCTION', // 插件
|
||||
SUGGESTION = 'SUGGESTION',
|
||||
RECOMMEND_QUESTIONS = 'RECOMMEND_QUESTIONS' // 推荐问题
|
||||
}
|
||||
|
||||
export type MessageItem = {
|
||||
@@ -41,3 +43,15 @@ export type DomainType = {
|
||||
name: string;
|
||||
bizName: string;
|
||||
};
|
||||
|
||||
export type SuggestionItemType = {
|
||||
id: number;
|
||||
domain: number;
|
||||
name: string;
|
||||
bizName: string;
|
||||
};
|
||||
|
||||
export type SuggestionType = {
|
||||
dimensions: SuggestionItemType[];
|
||||
metrics: SuggestionItemType[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user