Integrate Chat and Copilot into chat-sdk, and add SQL parse display (#166)

This commit is contained in:
williamhliu
2023-10-02 18:05:12 +08:00
committed by GitHub
parent 741ed4191b
commit 71cb20eb4f
68 changed files with 1353 additions and 882 deletions

View File

@@ -0,0 +1,48 @@
import LeftAvatar from '../CopilotAvatar';
import Message from '../Message';
import styles from './style.module.less';
import { AgentType } from '../../type';
import { isMobile } from '../../../utils/utils';
type Props = {
currentAgent?: AgentType;
onSendMsg: (value: string) => void;
};
const AgentTip: React.FC<Props> = ({ currentAgent, onSendMsg }) => {
if (!currentAgent) {
return null;
}
return (
<div className={styles.agentTip}>
{!isMobile && <LeftAvatar />}
<Message position="left" bubbleClassName={styles.agentTipMsg}>
<div className={styles.title}>
{currentAgent.name}
</div>
<div className={styles.content}>
<div className={styles.examples}>
{currentAgent.examples?.length > 0 ? (
currentAgent.examples.map(example => (
<div
key={example}
className={styles.example}
onClick={() => {
onSendMsg(example);
}}
>
{example}
</div>
))
) : (
<div className={styles.example}>{currentAgent.description}</div>
)}
</div>
</div>
</Message>
</div>
);
};
export default AgentTip;

View File

@@ -0,0 +1,44 @@
.agentTip {
display: flex;
.agentTipMsg {
padding: 12px 20px 20px !important;
.title {
margin-bottom: 12px;
font-size: 14px;
}
.content {
display: flex;
flex-direction: column;
flex-wrap: wrap;
margin-top: 10px;
column-gap: 14px;
.topBar {
.tip {
margin-top: 2px;
font-size: 13px;
}
}
.examples {
display: flex;
flex-direction: column;
font-size: 13px;
row-gap: 8px;
.example {
color: var(--chat-blue);
cursor: pointer;
}
}
&.fullscreen {
flex: none;
width: 280px;
}
}
}
}

View File

@@ -0,0 +1,66 @@
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';
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={`修改${CHAT_TITLE}问答名称`}
open={visible}
onCancel={onClose}
onOk={onConfirm}
confirmLoading={loading}
>
<Form {...layout} form={form}>
<FormItem name="conversationName" label="名称" rules={[{ required: true }]}>
<Input
placeholder={`请输入${CHAT_TITLE}问答名称`}
ref={conversationNameInputRef}
onPressEnter={onConfirm}
/>
</FormItem>
</Form>
</Modal>
);
};
export default ConversationModal;

View File

@@ -0,0 +1,8 @@
import IconFont from '../../../components/IconFont';
import styles from './style.module.less';
const CopilotAvatar = () => {
return <IconFont type="icon-zhinengsuanfa" className={styles.leftAvatar} />;
};
export default CopilotAvatar;

View File

@@ -0,0 +1,13 @@
.leftAvatar {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
margin-right: 6px;
margin-right: 6px;
color: var(--chat-blue);
font-size: 40px;
background-color: #fff;
border-radius: 50%;
}

View File

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

View File

@@ -0,0 +1,64 @@
import { useEffect, useState } from 'react';
import LeftAvatar from '../CopilotAvatar';
import Message from '../Message';
import styles from './style.module.less';
import { queryRecommendQuestions } from '../../service';
import { isMobile } from '../../../utils/utils';
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}>
{!isMobile && <LeftAvatar />}
{loading ? (
<></>
) : 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,36 @@
.recommendQuestions {
display: flex;
.recommendQuestionsMsg {
padding: 12px 20px 20px !important;
.title {
margin-bottom: 12px;
font-weight: 500;
font-size: 14px;
}
.content {
display: flex;
flex-wrap: wrap;
align-items: center;
column-gap: 16px;
row-gap: 20px;
.question {
height: 22px;
padding: 0 6px;
color: var(--text-color);
font-size: 12px;
line-height: 22px;
background-color: #f4f4f4;
border-radius: 11px;
cursor: pointer;
&:hover {
color: var(--chat-blue);
}
}
}
}
}

View File

@@ -0,0 +1,29 @@
import { isMobile } from '../../utils/utils';
import { Avatar } from 'antd';
import classNames from 'classnames';
import LeftAvatar from './CopilotAvatar';
import Message from './Message';
import styles from './style.module.less';
type Props = {
position: 'left' | 'right';
data: any;
quote?: string;
};
const Text: React.FC<Props> = ({ position, data, quote }) => {
const textWrapperClass = classNames(styles.textWrapper, {
[styles.rightTextWrapper]: position === 'right',
});
return (
<div className={textWrapperClass}>
{!isMobile && position === 'left' && <LeftAvatar />}
<Message position={position} bubbleClassName={styles.textBubble}>
{position === 'right' && quote && <div className={styles.quote}>{quote}</div>}
<div className={styles.text}>{data}</div>
</Message>
</div>
);
};
export default Text;

View File

@@ -0,0 +1,311 @@
.message {
.messageTitleBar {
display: flex;
align-items: baseline;
margin-bottom: 6px;
column-gap: 10px;
.modelName {
margin-left: 4px;
color: var(--text-color);
font-weight: 500;
}
.messageTopBar {
position: relative;
max-width: 80%;
overflow: hidden;
color: var(--text-color-third);
font-size: 13px;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.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: 14px;
background: linear-gradient(81.62deg, #2870ea 8.72%, var(--chat-blue) 85.01%);
border: 1px solid transparent;
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);
}
}
}
.textWrapper {
display: flex;
align-items: center;
&.rightTextWrapper {
justify-content: flex-end;
}
.rightAvatar {
margin-left: 6px;
}
}