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:
williamhliu
2023-07-31 12:00:39 +08:00
committed by GitHub
parent 0ac652c5d9
commit 7c99829052
68 changed files with 1429 additions and 1239 deletions

View File

@@ -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"

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;
}
}
}
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}
}
}
}

View File

@@ -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);

View File

@@ -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);
}
}
}
}
}
}

View File

@@ -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[];

View File

@@ -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;

View File

@@ -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);
}
}
}
}
}

View File

@@ -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;

View File

@@ -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;
}
}
}
}
}

View File

@@ -229,6 +229,7 @@
.typingBubble {
width: fit-content;
padding: 16px !important;
}
.quote {

View File

@@ -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>
);

View File

@@ -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',
});
}

View File

@@ -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;
}

View File

@@ -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[];
};